package securityevents

import (
	"context"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"encoding/base64"
	"encoding/json"
	"math/big"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
	"github.com/golang-jwt/jwt/v5"
	"github.com/google/uuid"
)

type fakeRepository struct {
	input     *store.ApplySessionRevokedInput
	dedup     map[string]struct{}
	confirmed bool
}

func (f *fakeRepository) ApplySessionRevoked(_ context.Context, input store.ApplySessionRevokedInput) (store.ApplySecurityEventResult, error) {
	if f.dedup == nil {
		f.dedup = make(map[string]struct{})
	}
	if _, exists := f.dedup[input.JTI]; exists {
		return store.ApplySecurityEventResult{Duplicate: true}, nil
	}
	f.dedup[input.JTI] = struct{}{}
	f.input = &input
	return store.ApplySecurityEventResult{WatermarkMoved: true, SessionsDeleted: 2}, nil
}

func (f *fakeRepository) ConfirmSecurityEventVerification(context.Context, string, string, string, string, []byte, time.Time) (bool, error) {
	f.confirmed = true
	return true, nil
}

func (*fakeRepository) RecordSecurityEventReceipt(context.Context, string, string, string, string, string) (bool, error) {
	return true, nil
}

func (*fakeRepository) ApplySecurityEventStreamUpdated(context.Context, string, string, string, string, string, string, time.Time) (bool, error) {
	return true, nil
}

func TestReceiverAcceptsValidSessionRevokedAndDuplicate(t *testing.T) {
	privateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	var transmitter *httptest.Server
	transmitter = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/.well-known/ssf-configuration/ssf":
			_ = json.NewEncoder(w).Encode(map[string]any{"issuer": transmitter.URL + "/ssf", "jwks_uri": transmitter.URL + "/ssf/jwks.json"})
		case "/ssf/jwks.json":
			_ = json.NewEncoder(w).Encode(map[string]any{"keys": []any{map[string]any{
				"kty": "EC", "kid": "current", "use": "sig", "alg": "ES256", "crv": "P-256",
				"x": coordinate(privateKey.X), "y": coordinate(privateKey.Y),
			}}})
		default:
			w.WriteHeader(http.StatusNotFound)
		}
	}))
	defer transmitter.Close()
	tenantID, subjectID, applicationID, streamID := uuid.NewString(), uuid.NewString(), uuid.NewString(), uuid.NewString()
	audience := "urn:easyai:ssf:receiver:" + applicationID
	verifier, err := NewVerifier(VerifierConfig{
		TransmitterIssuer: transmitter.URL + "/ssf", Audience: audience,
		SubjectIssuer: "https://auth.example/issuer/tenant-a", TenantID: tenantID, StreamID: streamID,
		ClockSkew: time.Minute,
	})
	if err != nil {
		t.Fatal(err)
	}
	repository := &fakeRepository{}
	handler, err := NewHandler(verifier, repository, transmitter.URL+"/ssf", audience, streamID, "receiver-bearer-secret", "next-receiver-secret")
	if err != nil {
		t.Fatal(err)
	}
	now := time.Now().UTC().Truncate(time.Second)
	jti := uuid.NewString()
	set := signSET(t, privateKey, map[string]any{
		"iss": transmitter.URL + "/ssf", "aud": audience, "iat": now.Unix(), "jti": jti, "txn": uuid.NewString(),
		"sub_id": map[string]any{
			"format": "complex",
			"user":   map[string]any{"format": "iss_sub", "iss": "https://auth.example/issuer/tenant-a", "sub": subjectID},
			"tenant": map[string]any{"format": "opaque", "id": tenantID},
		},
		"events": map[string]any{SessionRevokedEventType: map[string]any{"event_timestamp": now.Unix(), "initiating_entity": "admin"}},
	})
	for _, secret := range []string{"receiver-bearer-secret", "next-receiver-secret"} {
		response := httptest.NewRecorder()
		request := httptest.NewRequest(http.MethodPost, "/api/v1/security-events/ssf", strings.NewReader(set))
		request.Header.Set("Authorization", "Bearer "+secret)
		request.Header.Set("Content-Type", "application/secevent+jwt")
		handler.ServeHTTP(response, request)
		if response.Code != http.StatusAccepted || response.Body.Len() != 0 {
			t.Fatalf("status=%d body=%s", response.Code, response.Body.String())
		}
	}
	if repository.input == nil || repository.input.Subject != subjectID || repository.input.TenantID != tenantID ||
		repository.input.SubjectIssuer != "https://auth.example/issuer/tenant-a" {
		t.Fatalf("applied event = %#v", repository.input)
	}
}

func TestVerifierAcceptsApplicationScopedPrincipalAndTenantEvents(t *testing.T) {
	privateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	applicationID, tenantID, subjectID := uuid.NewString(), uuid.NewString(), uuid.NewString()
	verifier := &Verifier{config: VerifierConfig{
		TransmitterIssuer: "https://auth.example/ssf", Audience: "urn:easyai:ssf:receiver:" + applicationID,
		SubjectIssuer: "https://auth.example/issuer/shared", ApplicationID: applicationID,
		TenantMode: "multi_tenant", StreamID: uuid.NewString(), ClockSkew: time.Minute,
	}, keys: map[string]*ecdsa.PublicKey{"current": &privateKey.PublicKey}, expiresAt: time.Now().Add(time.Hour), client: http.DefaultClient}
	now := time.Now().UTC().Truncate(time.Second)
	claims := func(subjectType, subject, eventApplicationID string) jwt.MapClaims {
		return jwt.MapClaims{
			"iss": verifier.config.TransmitterIssuer, "aud": verifier.config.Audience, "iat": now.Unix(),
			"jti": uuid.NewString(), "txn": uuid.NewString(),
			"sub_id": map[string]any{
				"format": "complex",
				"user":   map[string]any{"format": "iss_sub", "iss": verifier.config.SubjectIssuer, "sub": subject},
				"tenant": map[string]any{"format": "opaque", "id": tenantID},
			},
			"events": map[string]any{SessionRevokedEventType: map[string]any{
				"event_timestamp": now.Unix(), "initiating_entity": "admin",
				"application_id": eventApplicationID, "subject_type": subjectType,
			}},
		}
	}
	for _, test := range []struct {
		name, subjectType, subject string
	}{
		{name: "principal", subjectType: "principal", subject: subjectID},
		{name: "tenant", subjectType: "tenant", subject: tenantID},
	} {
		t.Run(test.name, func(t *testing.T) {
			event, err := verifier.Verify(context.Background(), signSET(t, privateKey, claims(test.subjectType, test.subject, applicationID)))
			if err != nil || event.ApplicationID != applicationID || event.SubjectType != test.subjectType ||
				event.Subject != test.subject || event.TenantID != tenantID {
				t.Fatalf("event=%#v error=%v", event, err)
			}
		})
	}
	for _, invalid := range []jwt.MapClaims{
		claims("principal", subjectID, uuid.NewString()),
		claims("tenant", subjectID, applicationID),
	} {
		if _, err := verifier.Verify(context.Background(), signSET(t, privateKey, invalid)); err == nil {
			t.Fatal("invalid application-scoped event was accepted")
		}
	}
}

func TestReceiverRejectsAuthenticationMediaTypeAndForbiddenClaims(t *testing.T) {
	privateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	verifier := &Verifier{config: VerifierConfig{
		TransmitterIssuer: "https://auth.example/ssf", Audience: "urn:easyai:ssf:receiver:app",
		SubjectIssuer: "https://auth.example/issuer/tenant", TenantID: uuid.NewString(), StreamID: uuid.NewString(), ClockSkew: time.Minute,
	}, keys: map[string]*ecdsa.PublicKey{"current": &privateKey.PublicKey}, expiresAt: time.Now().Add(time.Hour), client: http.DefaultClient}
	handler, _ := NewHandler(verifier, &fakeRepository{}, "https://auth.example/ssf", verifier.config.Audience, verifier.config.StreamID, "receiver-bearer-secret", "")

	response := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/events", strings.NewReader("token"))
	request.Header.Set("Content-Type", "application/secevent+jwt")
	handler.ServeHTTP(response, request)
	if response.Code != http.StatusUnauthorized {
		t.Fatalf("missing bearer status=%d", response.Code)
	}
	assertSETError(t, response, "authentication_failed")

	set := signSET(t, privateKey, map[string]any{
		"iss": verifier.config.TransmitterIssuer, "aud": verifier.config.Audience, "iat": time.Now().Unix(),
		"jti": uuid.NewString(), "sub": "forbidden", "exp": time.Now().Add(time.Hour).Unix(), "events": map[string]any{},
	})
	response = httptest.NewRecorder()
	request = httptest.NewRequest(http.MethodPost, "/events", strings.NewReader(set))
	request.Header.Set("Authorization", "Bearer receiver-bearer-secret")
	request.Header.Set("Content-Type", "application/json")
	handler.ServeHTTP(response, request)
	if response.Code != http.StatusBadRequest {
		t.Fatalf("wrong media status=%d", response.Code)
	}
	assertSETError(t, response, "invalid_request")

	response = httptest.NewRecorder()
	request = httptest.NewRequest(http.MethodPost, "/events", strings.NewReader(set))
	request.Header.Set("Authorization", "Bearer receiver-bearer-secret")
	request.Header.Set("Content-Type", "application/secevent+jwt")
	handler.ServeHTTP(response, request)
	if response.Code != http.StatusBadRequest || !strings.Contains(response.Body.String(), "invalid_request") {
		t.Fatalf("forbidden claims status=%d body=%s", response.Code, response.Body.String())
	}

	future := time.Now().Add(10 * time.Minute)
	futureSET := signSET(t, privateKey, map[string]any{
		"iss": verifier.config.TransmitterIssuer, "aud": verifier.config.Audience, "iat": time.Now().Unix(),
		"jti": uuid.NewString(), "txn": uuid.NewString(),
		"sub_id": map[string]any{
			"format": "complex",
			"user":   map[string]any{"format": "iss_sub", "iss": verifier.config.SubjectIssuer, "sub": uuid.NewString()},
			"tenant": map[string]any{"format": "opaque", "id": verifier.config.TenantID},
		},
		"events": map[string]any{SessionRevokedEventType: map[string]any{
			"event_timestamp": future.Unix(), "initiating_entity": "admin",
		}},
	})
	response = httptest.NewRecorder()
	request = httptest.NewRequest(http.MethodPost, "/events", strings.NewReader(futureSET))
	request.Header.Set("Authorization", "Bearer receiver-bearer-secret")
	request.Header.Set("Content-Type", "application/secevent+jwt")
	handler.ServeHTTP(response, request)
	if response.Code != http.StatusBadRequest {
		t.Fatalf("future event timestamp status=%d", response.Code)
	}
}

func TestReceiverReturnsRFC8935IssuerAudienceAndKeyErrors(t *testing.T) {
	trustedKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	untrustedKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	verifier := &Verifier{config: VerifierConfig{
		TransmitterIssuer: "https://auth.example/ssf", Audience: "urn:easyai:ssf:receiver:app",
		SubjectIssuer: "https://auth.example/issuer/tenant", TenantID: uuid.NewString(), StreamID: uuid.NewString(), ClockSkew: time.Minute,
	}, keys: map[string]*ecdsa.PublicKey{"current": &trustedKey.PublicKey}, expiresAt: time.Now().Add(time.Hour), client: http.DefaultClient}
	handler, _ := NewHandler(verifier, &fakeRepository{}, verifier.config.TransmitterIssuer, verifier.config.Audience, verifier.config.StreamID, "receiver-bearer-secret", "")

	baseClaims := jwt.MapClaims{
		"iss": verifier.config.TransmitterIssuer, "aud": verifier.config.Audience, "iat": time.Now().Unix(),
		"jti": uuid.NewString(), "events": map[string]any{},
	}
	tests := []struct {
		name   string
		key    *ecdsa.PrivateKey
		mutate func(jwt.MapClaims)
		code   string
	}{
		{name: "issuer", key: trustedKey, mutate: func(claims jwt.MapClaims) { claims["iss"] = "https://wrong.example/ssf" }, code: "invalid_issuer"},
		{name: "audience", key: trustedKey, mutate: func(claims jwt.MapClaims) { claims["aud"] = "urn:wrong" }, code: "invalid_audience"},
		{name: "signature", key: untrustedKey, mutate: func(jwt.MapClaims) {}, code: "invalid_key"},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			claims := jwt.MapClaims{}
			for key, value := range baseClaims {
				claims[key] = value
			}
			test.mutate(claims)
			response := httptest.NewRecorder()
			request := httptest.NewRequest(http.MethodPost, "/events", strings.NewReader(signSET(t, test.key, claims)))
			request.Header.Set("Authorization", "Bearer receiver-bearer-secret")
			request.Header.Set("Content-Type", "application/secevent+jwt")
			handler.ServeHTTP(response, request)
			if response.Code != http.StatusBadRequest {
				t.Fatalf("status=%d body=%s", response.Code, response.Body.String())
			}
			assertSETError(t, response, test.code)
		})
	}
}

func TestReceiverAcceptsStreamUpdatedEvent(t *testing.T) {
	privateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	verifier := &Verifier{config: VerifierConfig{
		TransmitterIssuer: "https://auth.example/ssf", Audience: "urn:easyai:ssf:receiver:app",
		SubjectIssuer: "https://auth.example/issuer/tenant", TenantID: uuid.NewString(), StreamID: uuid.NewString(), ClockSkew: time.Minute,
	}, keys: map[string]*ecdsa.PublicKey{"current": &privateKey.PublicKey}, expiresAt: time.Now().Add(time.Hour), client: http.DefaultClient}
	handler, _ := NewHandler(verifier, &fakeRepository{}, verifier.config.TransmitterIssuer, verifier.config.Audience, verifier.config.StreamID, "receiver-bearer-secret", "")
	set := signSET(t, privateKey, jwt.MapClaims{
		"iss": verifier.config.TransmitterIssuer, "aud": verifier.config.Audience, "iat": time.Now().Unix(), "jti": uuid.NewString(),
		"sub_id": map[string]any{"format": "opaque", "id": verifier.config.StreamID},
		"events": map[string]any{StreamUpdatedEventType: map[string]any{"status": "paused", "reason": "maintenance"}},
	})
	response := httptest.NewRecorder()
	request := httptest.NewRequest(http.MethodPost, "/events", strings.NewReader(set))
	request.Header.Set("Authorization", "Bearer receiver-bearer-secret")
	request.Header.Set("Content-Type", "application/secevent+jwt")
	handler.ServeHTTP(response, request)
	if response.Code != http.StatusAccepted || response.Body.Len() != 0 {
		t.Fatalf("status=%d body=%s", response.Code, response.Body.String())
	}
}

func assertSETError(t *testing.T, response *httptest.ResponseRecorder, code string) {
	t.Helper()
	if response.Header().Get("Content-Language") != "en" {
		t.Fatalf("Content-Language=%q", response.Header().Get("Content-Language"))
	}
	var payload map[string]string
	if err := json.Unmarshal(response.Body.Bytes(), &payload); err != nil {
		t.Fatalf("invalid error body: %v", err)
	}
	if payload["err"] != code || payload["description"] == "" {
		t.Fatalf("error payload=%#v", payload)
	}
}

func signSET(t *testing.T, key *ecdsa.PrivateKey, claims jwt.MapClaims) string {
	t.Helper()
	token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
	token.Header["typ"], token.Header["kid"] = "secevent+jwt", "current"
	raw, err := token.SignedString(key)
	if err != nil {
		t.Fatal(err)
	}
	return raw
}

func coordinate(value *big.Int) string {
	payload := make([]byte, 32)
	value.FillBytes(payload)
	return base64.RawURLEncoding.EncodeToString(payload)
}
