package auth

import (
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/x509"
	"encoding/pem"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/go-jose/go-jose/v4"
	"github.com/go-jose/go-jose/v4/jwt"
)

func TestGoogleProviderConfigConfigured(t *testing.T) {
	empty := GoogleProviderConfig{}
	if empty.Configured() {
		t.Fatal("empty Google config must not be configured")
	}

	cfg := GoogleProviderConfig{
		ClientID:     "google-client",
		ClientSecret: "google-secret",
		RedirectURL:  "https://xtablo.test/auth/google/callback",
	}
	if !cfg.Configured() {
		t.Fatal("complete Google config must be configured")
	}
}

func TestOAuthStateAndNonceCookiesValidateExactValue(t *testing.T) {
	rec := httptest.NewRecorder()
	SetOAuthCookie(rec, "google", OAuthCookieState, "state-value", false)
	SetOAuthCookie(rec, "google", OAuthCookieNonce, "nonce-value", false)

	req := httptest.NewRequest(http.MethodGet, "/auth/google/callback", nil)
	for _, c := range rec.Result().Cookies() {
		req.AddCookie(c)
	}

	if !ValidateOAuthCookie(req, "google", OAuthCookieState, "state-value") {
		t.Fatal("state cookie should validate matching value")
	}
	if ValidateOAuthCookie(req, "google", OAuthCookieState, "wrong-state") {
		t.Fatal("state cookie should reject mismatched value")
	}
	if !ValidateOAuthCookie(req, "google", OAuthCookieNonce, "nonce-value") {
		t.Fatal("nonce cookie should validate matching value")
	}
	if ValidateOAuthCookie(req, "google", OAuthCookieNonce, "wrong-nonce") {
		t.Fatal("nonce cookie should reject mismatched value")
	}
}

func TestOAuthCookieNameIncludesProviderAndKind(t *testing.T) {
	if got := OAuthCookieName("google", OAuthCookieState); got != "xtablo_oauth_google_state" {
		t.Fatalf("state cookie name = %q", got)
	}
	if got := OAuthCookieName("google", OAuthCookieNonce); got != "xtablo_oauth_google_nonce" {
		t.Fatalf("nonce cookie name = %q", got)
	}
}

func TestAppleProviderConfigConfigured(t *testing.T) {
	empty := AppleProviderConfig{}
	if empty.Configured() {
		t.Fatal("empty Apple config must not be configured")
	}

	cfg := AppleProviderConfig{
		ClientID:    "com.xtablo.web",
		TeamID:      "TEAMID1234",
		KeyID:       "KEYID1234",
		PrivateKey:  testApplePrivateKeyPEM(t),
		RedirectURL: "https://xtablo.test/auth/apple/callback",
	}
	if !cfg.Configured() {
		t.Fatal("complete Apple config must be configured")
	}
}

func TestAppleClientSecretClaimsAndKeyID(t *testing.T) {
	now := time.Date(2026, 5, 15, 12, 0, 0, 0, time.UTC)
	privateKeyPEM := testApplePrivateKeyPEM(t)
	cfg := AppleProviderConfig{
		ClientID:    "com.xtablo.web",
		TeamID:      "TEAMID1234",
		KeyID:       "KEYID1234",
		PrivateKey:  stringsWithEscapedNewlines(privateKeyPEM),
		RedirectURL: "https://xtablo.test/auth/apple/callback",
	}

	secret, err := cfg.ClientSecret(now)
	if err != nil {
		t.Fatalf("ClientSecret: %v", err)
	}
	key := parseApplePrivateKeyForTest(t, privateKeyPEM)
	parsed, err := jwt.ParseSigned(secret, []jose.SignatureAlgorithm{jose.ES256})
	if err != nil {
		t.Fatalf("ParseSigned: %v", err)
	}
	var claims jwt.Claims
	if err := parsed.Claims(&key.PublicKey, &claims); err != nil {
		t.Fatalf("Claims: %v", err)
	}
	if claims.Issuer != "TEAMID1234" {
		t.Fatalf("iss = %q", claims.Issuer)
	}
	if claims.Subject != "com.xtablo.web" {
		t.Fatalf("sub = %q", claims.Subject)
	}
	if len(claims.Audience) != 1 || claims.Audience[0] != "https://appleid.apple.com" {
		t.Fatalf("aud = %#v", claims.Audience)
	}
	if !claims.Expiry.Time().After(now) {
		t.Fatalf("exp = %s; want after %s", claims.Expiry.Time(), now)
	}
	if parsed.Headers[0].KeyID != "KEYID1234" {
		t.Fatalf("kid = %q", parsed.Headers[0].KeyID)
	}
}

func testApplePrivateKeyPEM(t *testing.T) string {
	t.Helper()
	key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		t.Fatalf("GenerateKey: %v", err)
	}
	der, err := x509.MarshalECPrivateKey(key)
	if err != nil {
		t.Fatalf("MarshalECPrivateKey: %v", err)
	}
	return string(pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: der}))
}

func parseApplePrivateKeyForTest(t *testing.T, privateKeyPEM string) *ecdsa.PrivateKey {
	t.Helper()
	block, _ := pem.Decode([]byte(privateKeyPEM))
	if block == nil {
		t.Fatal("missing PEM block")
	}
	key, err := x509.ParseECPrivateKey(block.Bytes)
	if err != nil {
		t.Fatalf("ParseECPrivateKey: %v", err)
	}
	return key
}

func stringsWithEscapedNewlines(value string) string {
	out := ""
	for _, r := range value {
		if r == '\n' {
			out += `\n`
		} else {
			out += string(r)
		}
	}
	return out
}
