package service

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"errors"
	"io"
	"log/slog"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"testing"
	"time"

	"wucher/internal/config"
	"wucher/internal/resilience"
)

func TestNewMicrosoftSSO(t *testing.T) {
	cfg := config.MicrosoftSSOConfig{
		TenantID:     "tenant-id",
		ClientID:     "client-id",
		ClientSecret: "secret",
		RedirectURL:  "http://localhost/callback",
		Authority:    "https://login.microsoftonline.com/",
		Scopes:       []string{"openid", "profile", "email"},
	}

	client := NewMicrosoftSSO(cfg)
	if client == nil {
		t.Fatalf("expected client")
	}
	if client.cfg.ClientID != cfg.ClientID {
		t.Fatalf("client config not set")
	}
	if client.client == nil {
		t.Fatalf("http client should not be nil")
	}
	if client.client.Timeout != 10*time.Second {
		t.Fatalf("unexpected timeout: %v", client.client.Timeout)
	}
}

func TestMicrosoftSSO_AuthCodeURL(t *testing.T) {
	cfg := config.MicrosoftSSOConfig{
		TenantID:    "tenant-id",
		ClientID:    "client-id",
		RedirectURL: "http://localhost/callback",
		Authority:   "https://login.microsoftonline.com/",
		Scopes:      []string{"openid", "profile", "email"},
	}
	client := NewMicrosoftSSO(cfg)

	raw := client.AuthCodeURL("state-123")
	parsed, err := url.Parse(raw)
	if err != nil {
		t.Fatalf("parse url: %v", err)
	}
	if parsed.Path != "/tenant-id/oauth2/v2.0/authorize" {
		t.Fatalf("unexpected authorize path: %s", parsed.Path)
	}
	q := parsed.Query()
	if q.Get("client_id") != "client-id" ||
		q.Get("response_type") != "code" ||
		q.Get("redirect_uri") != "http://localhost/callback" ||
		q.Get("response_mode") != "query" ||
		q.Get("scope") != "openid profile email offline_access User.Read" ||
		q.Get("prompt") != "select_account" ||
		q.Get("state") != "state-123" {
		t.Fatalf("unexpected query values: %v", q)
	}
	if got := q.Get("max_age"); got != "" {
		t.Fatalf("max_age should not be set, got %q", got)
	}
}

func TestMicrosoftSSO_LogoutURL(t *testing.T) {
	cfg := config.MicrosoftSSOConfig{
		TenantID:    "tenant-id",
		ClientID:    "client-id",
		RedirectURL: "http://localhost/callback",
		Authority:   "https://login.microsoftonline.com/",
		Scopes:      []string{"openid", "profile", "email"},
	}
	client := NewMicrosoftSSO(cfg)

	raw := client.LogoutURL("http://localhost/logout-done")
	parsed, err := url.Parse(raw)
	if err != nil {
		t.Fatalf("parse url: %v", err)
	}
	if parsed.Path != "/tenant-id/oauth2/v2.0/logout" {
		t.Fatalf("unexpected logout path: %s", parsed.Path)
	}
	if got := parsed.Query().Get("post_logout_redirect_uri"); got != "http://localhost/logout-done" {
		t.Fatalf("unexpected post logout redirect: %q", got)
	}
}

func TestMicrosoftSSO_ExchangeCode(t *testing.T) {
	t.Run("missing code", func(t *testing.T) {
		client := NewMicrosoftSSO(config.MicrosoftSSOConfig{})
		if _, err := client.ExchangeCode(context.Background(), ""); err == nil {
			t.Fatalf("expected error for missing code")
		}
	})

	t.Run("token endpoint non 2xx", func(t *testing.T) {
		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			http.Error(w, "bad request", http.StatusBadRequest)
		}))
		defer server.Close()

		client := NewMicrosoftSSO(config.MicrosoftSSOConfig{
			TenantID:     "tenant-id",
			ClientID:     "client-id",
			ClientSecret: "secret",
			RedirectURL:  "http://localhost/callback",
			Authority:    server.URL + "/",
			Scopes:       []string{"openid"},
		})

		if _, err := client.ExchangeCode(context.Background(), "code-1"); err == nil || !strings.Contains(err.Error(), "token exchange failed") {
			t.Fatalf("expected token exchange failed error, got %v", err)
		}
	})

	t.Run("invalid json response", func(t *testing.T) {
		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			w.Header().Set("Content-Type", "application/json")
			_, _ = w.Write([]byte(`{invalid`))
		}))
		defer server.Close()

		client := NewMicrosoftSSO(config.MicrosoftSSOConfig{
			TenantID:     "tenant-id",
			ClientID:     "client-id",
			ClientSecret: "secret",
			RedirectURL:  "http://localhost/callback",
			Authority:    server.URL + "/",
			Scopes:       []string{"openid"},
		})

		if _, err := client.ExchangeCode(context.Background(), "code-1"); err == nil {
			t.Fatalf("expected unmarshal error")
		}
	})

	t.Run("missing id token", func(t *testing.T) {
		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			w.Header().Set("Content-Type", "application/json")
			_, _ = w.Write([]byte(`{"access_token":"abc"}`))
		}))
		defer server.Close()

		client := NewMicrosoftSSO(config.MicrosoftSSOConfig{
			TenantID:     "tenant-id",
			ClientID:     "client-id",
			ClientSecret: "secret",
			RedirectURL:  "http://localhost/callback",
			Authority:    server.URL + "/",
			Scopes:       []string{"openid"},
		})

		if _, err := client.ExchangeCode(context.Background(), "code-1"); err == nil || !strings.Contains(err.Error(), "missing id_token") {
			t.Fatalf("expected missing id_token error, got %v", err)
		}
	})

	t.Run("invalid id token claims", func(t *testing.T) {
		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			w.Header().Set("Content-Type", "application/json")
			_, _ = w.Write([]byte(`{"id_token":"invalid-token"}`))
		}))
		defer server.Close()

		client := NewMicrosoftSSO(config.MicrosoftSSOConfig{
			TenantID:     "tenant-id",
			ClientID:     "client-id",
			ClientSecret: "secret",
			RedirectURL:  "http://localhost/callback",
			Authority:    server.URL + "/",
			Scopes:       []string{"openid"},
		})

		if _, err := client.ExchangeCode(context.Background(), "code-1"); err == nil {
			t.Fatalf("expected parse jwt error")
		}
	})

	t.Run("success with email claim", func(t *testing.T) {
		idToken := makeUnsignedJWT(map[string]any{
			"sub":   "subject-1",
			"email": "user@example.com",
			"name":  "John Doe",
		})
		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			if r.Method != http.MethodPost {
				t.Fatalf("expected POST")
			}
			if r.URL.Path != "/tenant-id/oauth2/v2.0/token" {
				t.Fatalf("unexpected path: %s", r.URL.Path)
			}
			if err := r.ParseForm(); err != nil {
				t.Fatalf("parse form: %v", err)
			}
			if r.PostForm.Get("client_id") != "client-id" ||
				r.PostForm.Get("client_secret") != "secret" ||
				r.PostForm.Get("grant_type") != "authorization_code" ||
				r.PostForm.Get("code") != "code-1" ||
				r.PostForm.Get("redirect_uri") != "http://localhost/callback" ||
				r.PostForm.Get("scope") != "openid profile email offline_access User.Read" {
				t.Fatalf("unexpected form values: %v", r.PostForm)
			}
			w.Header().Set("Content-Type", "application/json")
			_ = json.NewEncoder(w).Encode(map[string]string{"id_token": idToken, "access_token": "at-1"})
		}))
		defer server.Close()

		client := NewMicrosoftSSO(config.MicrosoftSSOConfig{
			TenantID:     "tenant-id",
			ClientID:     "client-id",
			ClientSecret: "secret",
			RedirectURL:  "http://localhost/callback",
			Authority:    server.URL + "/",
			Scopes:       []string{"openid", "profile", "email"},
		})

		claims, err := client.ExchangeCode(context.Background(), "code-1")
		if err != nil {
			t.Fatalf("exchange code failed: %v", err)
		}
		if claims.Subject != "subject-1" || claims.Email != "user@example.com" || claims.Name != "John Doe" {
			t.Fatalf("unexpected claims: %+v", claims)
		}
	})

	t.Run("success fallback preferred username", func(t *testing.T) {
		idToken := makeUnsignedJWT(map[string]any{
			"sub":                "subject-2",
			"preferred_username": "fallback@example.com",
			"name":               "Jane Doe",
		})
		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			w.Header().Set("Content-Type", "application/json")
			_ = json.NewEncoder(w).Encode(map[string]string{"id_token": idToken, "access_token": "at-2"})
		}))
		defer server.Close()

		client := NewMicrosoftSSO(config.MicrosoftSSOConfig{
			TenantID:     "tenant-id",
			ClientID:     "client-id",
			ClientSecret: "secret",
			RedirectURL:  "http://localhost/callback",
			Authority:    server.URL + "/",
			Scopes:       []string{"openid"},
		})

		claims, err := client.ExchangeCode(context.Background(), "code-1")
		if err != nil {
			t.Fatalf("exchange code failed: %v", err)
		}
		if claims.Email != "fallback@example.com" {
			t.Fatalf("expected preferred_username fallback, got %q", claims.Email)
		}
	})

	t.Run("http client do error", func(t *testing.T) {
		client := NewMicrosoftSSO(config.MicrosoftSSOConfig{
			TenantID:     "tenant-id",
			ClientID:     "client-id",
			ClientSecret: "secret",
			RedirectURL:  "http://localhost/callback",
			Authority:    "https://example.com/",
			Scopes:       []string{"openid"},
		})
		client.client = &http.Client{
			Transport: roundTripFunc(func(*http.Request) (*http.Response, error) {
				return nil, errors.New("network down")
			}),
		}

		if _, err := client.ExchangeCode(context.Background(), "code-1"); err == nil || !strings.Contains(err.Error(), "network down") {
			t.Fatalf("expected http client error, got %v", err)
		}
	})
}

func TestMicrosoftSSO_ExchangeCode_CircuitBreaker(t *testing.T) {
	newExecutor := func() *resilience.Executor {
		return resilience.NewExecutor("microsoft_sso", config.CircuitBreakerPolicy{
			Enabled:             true,
			Timeout:             time.Second,
			MinRequests:         2,
			FailureRatio:        0.5,
			ConsecutiveFailures: 2,
		}, slog.New(slog.NewJSONHandler(io.Discard, nil)), resilience.ClassifyMicrosoftSSOError)
	}

	t.Run("client errors are excluded", func(t *testing.T) {
		hits := 0
		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			hits++
			http.Error(w, "bad request", http.StatusBadRequest)
		}))
		defer server.Close()

		client := NewMicrosoftSSO(config.MicrosoftSSOConfig{
			TenantID:     "tenant-id",
			ClientID:     "client-id",
			ClientSecret: "secret",
			RedirectURL:  "http://localhost/callback",
			Authority:    server.URL + "/",
			Scopes:       []string{"openid"},
		}).WithExecutor(newExecutor())

		for range 3 {
			if _, err := client.ExchangeCode(context.Background(), "code-1"); err == nil || !strings.Contains(err.Error(), "token exchange failed") {
				t.Fatalf("expected token exchange failed error, got %v", err)
			}
		}
		if hits != 3 {
			t.Fatalf("expected breaker to stay closed for excluded client errors, got %d hits", hits)
		}
	})

	t.Run("server errors open the breaker", func(t *testing.T) {
		hits := 0
		server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			hits++
			http.Error(w, "unavailable", http.StatusServiceUnavailable)
		}))
		defer server.Close()

		client := NewMicrosoftSSO(config.MicrosoftSSOConfig{
			TenantID:     "tenant-id",
			ClientID:     "client-id",
			ClientSecret: "secret",
			RedirectURL:  "http://localhost/callback",
			Authority:    server.URL + "/",
			Scopes:       []string{"openid"},
		}).WithExecutor(newExecutor())

		for range 2 {
			if _, err := client.ExchangeCode(context.Background(), "code-1"); err == nil || !strings.Contains(err.Error(), "token exchange failed") {
				t.Fatalf("expected token exchange failed error, got %v", err)
			}
		}
		_, err := client.ExchangeCode(context.Background(), "code-1")
		var openErr *resilience.OpenError
		if !errors.As(err, &openErr) {
			t.Fatalf("expected circuit open error, got %v", err)
		}
		if hits != 2 {
			t.Fatalf("expected breaker to fail fast after opening, got %d hits", hits)
		}
	})
}

func TestParseJWTClaims(t *testing.T) {
	t.Run("invalid jwt format", func(t *testing.T) {
		if _, err := parseJWTClaims("invalid"); err == nil {
			t.Fatalf("expected invalid jwt error")
		}
	})

	t.Run("invalid payload encoding", func(t *testing.T) {
		if _, err := parseJWTClaims("header.%%%.sig"); err == nil {
			t.Fatalf("expected decode error")
		}
	})

	t.Run("invalid payload json", func(t *testing.T) {
		payload := base64.RawURLEncoding.EncodeToString([]byte("not-json"))
		if _, err := parseJWTClaims("header." + payload + ".sig"); err == nil {
			t.Fatalf("expected json unmarshal error")
		}
	})

	t.Run("success", func(t *testing.T) {
		token := makeUnsignedJWT(map[string]any{
			"sub":   "subject-1",
			"email": "user@example.com",
			"name":  "John Doe",
		})
		claims, err := parseJWTClaims(token)
		if err != nil {
			t.Fatalf("parse claims failed: %v", err)
		}
		if claims["sub"] != "subject-1" || claims["email"] != "user@example.com" {
			t.Fatalf("unexpected claims: %+v", claims)
		}
	})
}

func TestDecodeSegment(t *testing.T) {
	t.Run("success", func(t *testing.T) {
		raw := base64.RawURLEncoding.EncodeToString([]byte("hello"))
		got, err := decodeSegment(raw)
		if err != nil {
			t.Fatalf("decode failed: %v", err)
		}
		if string(got) != "hello" {
			t.Fatalf("unexpected value: %q", string(got))
		}
	})

	t.Run("success url safe alphabet", func(t *testing.T) {
		got, err := decodeSegment("--__")
		if err != nil {
			t.Fatalf("decode failed: %v", err)
		}
		want := []byte{0xfb, 0xef, 0xff}
		if len(got) != len(want) {
			t.Fatalf("unexpected length: %d", len(got))
		}
		for i := range want {
			if got[i] != want[i] {
				t.Fatalf("unexpected byte at %d: got %x want %x", i, got[i], want[i])
			}
		}
	})

	t.Run("invalid base64", func(t *testing.T) {
		if _, err := decodeSegment("%"); err == nil {
			t.Fatalf("expected decode error")
		}
	})
}

type roundTripFunc func(req *http.Request) (*http.Response, error)

func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { return f(req) }

func makeUnsignedJWT(claims map[string]any) string {
	header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"none","typ":"JWT"}`))
	payloadBytes, _ := json.Marshal(claims)
	payload := base64.RawURLEncoding.EncodeToString(payloadBytes)
	return header + "." + payload + ".sig"
}
