package service

import (
	"context"
	"testing"
	"time"

	"github.com/shankar0123/certctl/internal/domain"
)

// helper to create a test CertificateService wired for revocation tests
func newRevocationTestService() (*CertificateService, *mockCertRepo, *mockRevocationRepo, *mockAuditRepo) {
	certRepo := newMockCertificateRepository()
	auditRepo := newMockAuditRepository()
	policyRepo := newMockPolicyRepository()
	revocationRepo := newMockRevocationRepository()
	profileRepo := newMockProfileRepository()

	auditService := NewAuditService(auditRepo)
	policyService := NewPolicyService(policyRepo, auditService)

	// Create RevocationSvc
	revSvc := NewRevocationSvc(certRepo, revocationRepo, auditService)
	revSvc.SetIssuerRegistry(map[string]IssuerConnector{
		"iss-local": &mockIssuerConnector{},
	})

	// Create CAOperationsSvc
	caSvc := NewCAOperationsSvc(revocationRepo, certRepo, profileRepo)
	caSvc.SetIssuerRegistry(map[string]IssuerConnector{
		"iss-local": &mockIssuerConnector{},
	})

	certService := NewCertificateService(certRepo, policyService, auditService)
	certService.SetRevocationSvc(revSvc)
	certService.SetCAOperationsSvc(caSvc)

	return certService, certRepo, revocationRepo, auditRepo
}

func TestRevokeCertificate_Success(t *testing.T) {
	svc, certRepo, revocationRepo, auditRepo := newRevocationTestService()

	// Set up test data
	cert := &domain.ManagedCertificate{
		ID:         "cert-1",
		CommonName: "example.com",
		IssuerID:   "iss-local",
		Status:     domain.CertificateStatusActive,
		ExpiresAt:  time.Now().AddDate(0, 6, 0),
	}
	certRepo.AddCert(cert)

	// Add a certificate version with a serial number
	version := &domain.CertificateVersion{
		ID:            "ver-1",
		CertificateID: "cert-1",
		SerialNumber:  "ABC123",
		NotBefore:     time.Now(),
		NotAfter:      time.Now().AddDate(1, 0, 0),
		CreatedAt:     time.Now(),
	}
	certRepo.Versions["cert-1"] = []*domain.CertificateVersion{version}

	// Revoke
	err := svc.RevokeCertificateWithActor(context.Background(), "cert-1", "keyCompromise", "admin")
	if err != nil {
		t.Fatalf("expected no error, got: %v", err)
	}

	// Verify certificate status changed
	updated, _ := certRepo.Get(context.Background(), "cert-1")
	if updated.Status != domain.CertificateStatusRevoked {
		t.Errorf("expected status Revoked, got %s", updated.Status)
	}
	if updated.RevokedAt == nil {
		t.Error("expected RevokedAt to be set")
	}
	if updated.RevocationReason != "keyCompromise" {
		t.Errorf("expected reason keyCompromise, got %s", updated.RevocationReason)
	}

	// Verify revocation record created
	if len(revocationRepo.Revocations) != 1 {
		t.Fatalf("expected 1 revocation record, got %d", len(revocationRepo.Revocations))
	}
	rev := revocationRepo.Revocations[0]
	if rev.SerialNumber != "ABC123" {
		t.Errorf("expected serial ABC123, got %s", rev.SerialNumber)
	}
	if rev.Reason != "keyCompromise" {
		t.Errorf("expected reason keyCompromise, got %s", rev.Reason)
	}
	if rev.RevokedBy != "admin" {
		t.Errorf("expected revokedBy admin, got %s", rev.RevokedBy)
	}

	// Verify audit event recorded
	if len(auditRepo.Events) == 0 {
		t.Error("expected audit event to be recorded")
	}
	foundRevocationAudit := false
	for _, e := range auditRepo.Events {
		if e.Action == "certificate_revoked" {
			foundRevocationAudit = true
		}
	}
	if !foundRevocationAudit {
		t.Error("expected certificate_revoked audit event")
	}
}

func TestRevokeCertificate_DefaultReason(t *testing.T) {
	svc, certRepo, revocationRepo, _ := newRevocationTestService()

	cert := &domain.ManagedCertificate{
		ID:         "cert-2",
		CommonName: "default-reason.com",
		IssuerID:   "iss-local",
		Status:     domain.CertificateStatusActive,
		ExpiresAt:  time.Now().AddDate(0, 6, 0),
	}
	certRepo.AddCert(cert)
	certRepo.Versions["cert-2"] = []*domain.CertificateVersion{
		{ID: "ver-2", CertificateID: "cert-2", SerialNumber: "DEF456", CreatedAt: time.Now()},
	}

	// Revoke with empty reason — should default to "unspecified"
	err := svc.RevokeCertificateWithActor(context.Background(), "cert-2", "", "api")
	if err != nil {
		t.Fatalf("expected no error, got: %v", err)
	}

	updated, _ := certRepo.Get(context.Background(), "cert-2")
	if updated.RevocationReason != "unspecified" {
		t.Errorf("expected default reason 'unspecified', got %s", updated.RevocationReason)
	}

	if len(revocationRepo.Revocations) != 1 {
		t.Fatalf("expected 1 revocation, got %d", len(revocationRepo.Revocations))
	}
	if revocationRepo.Revocations[0].Reason != "unspecified" {
		t.Errorf("expected revocation reason 'unspecified', got %s", revocationRepo.Revocations[0].Reason)
	}
}

func TestRevokeCertificate_AlreadyRevoked(t *testing.T) {
	svc, certRepo, _, _ := newRevocationTestService()

	now := time.Now()
	cert := &domain.ManagedCertificate{
		ID:               "cert-3",
		CommonName:       "already-revoked.com",
		IssuerID:         "iss-local",
		Status:           domain.CertificateStatusRevoked,
		RevokedAt:        &now,
		RevocationReason: "keyCompromise",
		ExpiresAt:        time.Now().AddDate(0, 6, 0),
	}
	certRepo.AddCert(cert)

	err := svc.RevokeCertificateWithActor(context.Background(), "cert-3", "superseded", "admin")
	if err == nil {
		t.Fatal("expected error for already revoked certificate")
	}
	if err.Error() != "certificate is already revoked" {
		t.Errorf("expected 'already revoked' error, got: %v", err)
	}
}

func TestRevokeCertificate_ArchivedCert(t *testing.T) {
	svc, certRepo, _, _ := newRevocationTestService()

	cert := &domain.ManagedCertificate{
		ID:         "cert-4",
		CommonName: "archived.com",
		IssuerID:   "iss-local",
		Status:     domain.CertificateStatusArchived,
		ExpiresAt:  time.Now().AddDate(0, 6, 0),
	}
	certRepo.AddCert(cert)

	err := svc.RevokeCertificateWithActor(context.Background(), "cert-4", "keyCompromise", "admin")
	if err == nil {
		t.Fatal("expected error for archived certificate")
	}
	if err.Error() != "cannot revoke archived certificate" {
		t.Errorf("expected 'cannot revoke archived' error, got: %v", err)
	}
}

func TestRevokeCertificate_InvalidReason(t *testing.T) {
	svc, certRepo, _, _ := newRevocationTestService()

	cert := &domain.ManagedCertificate{
		ID:         "cert-5",
		CommonName: "invalid-reason.com",
		IssuerID:   "iss-local",
		Status:     domain.CertificateStatusActive,
		ExpiresAt:  time.Now().AddDate(0, 6, 0),
	}
	certRepo.AddCert(cert)

	err := svc.RevokeCertificateWithActor(context.Background(), "cert-5", "notAValidReason", "admin")
	if err == nil {
		t.Fatal("expected error for invalid reason")
	}
	if err.Error() != "invalid revocation reason: notAValidReason" {
		t.Errorf("unexpected error: %v", err)
	}
}

func TestRevokeCertificate_NotFound(t *testing.T) {
	svc, _, _, _ := newRevocationTestService()

	err := svc.RevokeCertificateWithActor(context.Background(), "nonexistent-cert", "keyCompromise", "admin")
	if err == nil {
		t.Fatal("expected error for nonexistent certificate")
	}
}

func TestRevokeCertificate_NoVersion(t *testing.T) {
	svc, certRepo, _, _ := newRevocationTestService()

	cert := &domain.ManagedCertificate{
		ID:         "cert-6",
		CommonName: "no-version.com",
		IssuerID:   "iss-local",
		Status:     domain.CertificateStatusActive,
		ExpiresAt:  time.Now().AddDate(0, 6, 0),
	}
	certRepo.AddCert(cert)
	// No versions added — should fail

	err := svc.RevokeCertificateWithActor(context.Background(), "cert-6", "keyCompromise", "admin")
	if err == nil {
		t.Fatal("expected error when no certificate version exists")
	}
}

func TestRevokeCertificate_WithIssuerNotification(t *testing.T) {
	svc, certRepo, revocationRepo, _ := newRevocationTestService()

	// Wire up issuer registry on RevocationSvc with mock
	mockIssuer := &mockIssuerConnector{}
	svc.revSvc.SetIssuerRegistry(map[string]IssuerConnector{
		"iss-local": mockIssuer,
	})

	cert := &domain.ManagedCertificate{
		ID:         "cert-7",
		CommonName: "issuer-notify.com",
		IssuerID:   "iss-local",
		Status:     domain.CertificateStatusActive,
		ExpiresAt:  time.Now().AddDate(0, 6, 0),
	}
	certRepo.AddCert(cert)
	certRepo.Versions["cert-7"] = []*domain.CertificateVersion{
		{ID: "ver-7", CertificateID: "cert-7", SerialNumber: "GHI789", CreatedAt: time.Now()},
	}

	err := svc.RevokeCertificateWithActor(context.Background(), "cert-7", "cessationOfOperation", "admin")
	if err != nil {
		t.Fatalf("expected no error, got: %v", err)
	}

	// Verify revocation was recorded and issuer was notified
	if len(revocationRepo.Revocations) != 1 {
		t.Fatalf("expected 1 revocation, got %d", len(revocationRepo.Revocations))
	}
	if !revocationRepo.Revocations[0].IssuerNotified {
		t.Error("expected issuer to be marked as notified")
	}
}

func TestRevokeCertificate_WithNotificationService(t *testing.T) {
	svc, certRepo, _, _ := newRevocationTestService()

	// Wire up notification service on RevocationSvc
	notifRepo := newMockNotificationRepository()
	notifService := NewNotificationService(notifRepo, make(map[string]Notifier))
	svc.revSvc.SetNotificationService(notifService)

	cert := &domain.ManagedCertificate{
		ID:         "cert-8",
		CommonName: "with-notify.com",
		IssuerID:   "iss-local",
		Status:     domain.CertificateStatusActive,
		OwnerID:    "owner-alice",
		ExpiresAt:  time.Now().AddDate(0, 6, 0),
	}
	certRepo.AddCert(cert)
	certRepo.Versions["cert-8"] = []*domain.CertificateVersion{
		{ID: "ver-8", CertificateID: "cert-8", SerialNumber: "JKL012", CreatedAt: time.Now()},
	}

	err := svc.RevokeCertificateWithActor(context.Background(), "cert-8", "keyCompromise", "admin")
	if err != nil {
		t.Fatalf("expected no error, got: %v", err)
	}

	// Should have created revocation notifications (webhook + email)
	if len(notifRepo.Notifications) < 1 {
		t.Error("expected at least one revocation notification to be created")
	}

	foundRevocationNotif := false
	for _, n := range notifRepo.Notifications {
		if n.Type == domain.NotificationTypeRevocation {
			foundRevocationNotif = true
		}
	}
	if !foundRevocationNotif {
		t.Error("expected Revocation type notification")
	}
}

func TestRevokeCertificate_AllValidReasons(t *testing.T) {
	reasons := []string{
		"unspecified", "keyCompromise", "caCompromise", "affiliationChanged",
		"superseded", "cessationOfOperation", "certificateHold", "privilegeWithdrawn",
	}

	for _, reason := range reasons {
		t.Run(reason, func(t *testing.T) {
			svc, certRepo, _, _ := newRevocationTestService()

			cert := &domain.ManagedCertificate{
				ID:         "cert-" + reason,
				CommonName: reason + ".com",
				IssuerID:   "iss-local",
				Status:     domain.CertificateStatusActive,
				ExpiresAt:  time.Now().AddDate(0, 6, 0),
			}
			certRepo.AddCert(cert)
			certRepo.Versions["cert-"+reason] = []*domain.CertificateVersion{
				{ID: "ver-" + reason, CertificateID: "cert-" + reason, SerialNumber: "SER-" + reason, CreatedAt: time.Now()},
			}

			err := svc.RevokeCertificateWithActor(context.Background(), "cert-"+reason, reason, "admin")
			if err != nil {
				t.Fatalf("expected no error for reason %s, got: %v", reason, err)
			}

			updated, _ := certRepo.Get(context.Background(), "cert-"+reason)
			if updated.Status != domain.CertificateStatusRevoked {
				t.Errorf("expected Revoked status, got %s", updated.Status)
			}
		})
	}
}

func TestGetRevokedCertificates_Success(t *testing.T) {
	svc, _, revocationRepo, _ := newRevocationTestService()

	// Pre-populate revocation records
	revocationRepo.Revocations = []*domain.CertificateRevocation{
		{ID: "rev-1", CertificateID: "cert-1", SerialNumber: "SER-1", Reason: "keyCompromise", RevokedAt: time.Now()},
		{ID: "rev-2", CertificateID: "cert-2", SerialNumber: "SER-2", Reason: "superseded", RevokedAt: time.Now()},
	}

	revocations, err := svc.GetRevokedCertificates()
	if err != nil {
		t.Fatalf("expected no error, got: %v", err)
	}
	if len(revocations) != 2 {
		t.Errorf("expected 2 revocations, got %d", len(revocations))
	}
}

func TestGetRevokedCertificates_Empty(t *testing.T) {
	svc, _, _, _ := newRevocationTestService()

	revocations, err := svc.GetRevokedCertificates()
	if err != nil {
		t.Fatalf("expected no error, got: %v", err)
	}
	if revocations == nil {
		// nil is acceptable for empty
	} else if len(revocations) != 0 {
		t.Errorf("expected 0 revocations, got %d", len(revocations))
	}
}

func TestGetRevokedCertificates_NoRepo(t *testing.T) {
	certRepo := newMockCertificateRepository()
	auditRepo := newMockAuditRepository()
	policyRepo := newMockPolicyRepository()
	auditService := NewAuditService(auditRepo)
	policyService := NewPolicyService(policyRepo, auditService)
	svc := NewCertificateService(certRepo, policyService, auditService)
	// Do NOT set revocation repo

	_, err := svc.GetRevokedCertificates()
	if err == nil {
		t.Fatal("expected error when revocation repo not configured")
	}
}

func TestRevokeCertificate_HandlerInterfaceMethod(t *testing.T) {
	svc, certRepo, _, _ := newRevocationTestService()

	cert := &domain.ManagedCertificate{
		ID:         "cert-handler",
		CommonName: "handler-test.com",
		IssuerID:   "iss-local",
		Status:     domain.CertificateStatusActive,
		ExpiresAt:  time.Now().AddDate(0, 6, 0),
	}
	certRepo.AddCert(cert)
	certRepo.Versions["cert-handler"] = []*domain.CertificateVersion{
		{ID: "ver-handler", CertificateID: "cert-handler", SerialNumber: "SER-HANDLER", CreatedAt: time.Now()},
	}

	// Test the handler interface method (no actor param)
	err := svc.RevokeCertificate("cert-handler", "superseded")
	if err != nil {
		t.Fatalf("expected no error, got: %v", err)
	}

	updated, _ := certRepo.Get(context.Background(), "cert-handler")
	if updated.Status != domain.CertificateStatusRevoked {
		t.Errorf("expected Revoked status, got %s", updated.Status)
	}
}

// M15b: CRL and OCSP Service Tests

func TestGenerateDERCRL_Success(t *testing.T) {
	svc, _, revocationRepo, _ := newRevocationTestService()

	// Add some revoked certificates to the repo
	now := time.Now()
	revocationRepo.Revocations = []*domain.CertificateRevocation{
		{
			SerialNumber:  "SERIAL-001",
			CertificateID: "cert-1",
			IssuerID:      "iss-local",
			Reason:        "keyCompromise",
			RevokedAt:     now.Add(-24 * time.Hour),
			RevokedBy:     "admin",
		},
		{
			SerialNumber:  "SERIAL-002",
			CertificateID: "cert-2",
			IssuerID:      "iss-local",
			Reason:        "superseded",
			RevokedAt:     now.Add(-12 * time.Hour),
			RevokedBy:     "admin",
		},
	}

	crl, err := svc.GenerateDERCRL("iss-local")

	if err != nil {
		t.Fatalf("expected no error, got: %v", err)
	}

	if crl == nil {
		t.Fatal("expected non-nil CRL")
	}

	if len(crl) == 0 {
		t.Fatal("expected non-empty CRL")
	}

	t.Logf("DER CRL generated successfully: %d bytes", len(crl))
}

func TestGenerateDERCRL_EmptyCRL(t *testing.T) {
	svc, _, revocationRepo, _ := newRevocationTestService()

	// No revoked certs for this issuer
	revocationRepo.Revocations = []*domain.CertificateRevocation{}

	crl, err := svc.GenerateDERCRL("iss-local")

	if err != nil {
		t.Fatalf("expected no error, got: %v", err)
	}

	if crl == nil {
		t.Fatal("expected non-nil CRL even when empty")
	}

	if len(crl) == 0 {
		t.Fatal("expected non-empty CRL bytes (at least the CRL structure)")
	}

	t.Logf("Empty DER CRL generated successfully: %d bytes", len(crl))
}

func TestGenerateDERCRL_IssuerNotFound(t *testing.T) {
	svc, _, _, _ := newRevocationTestService()

	// Try to generate CRL for unknown issuer
	crl, err := svc.GenerateDERCRL("iss-unknown")

	// Should return error or nil CRL depending on implementation
	if crl != nil && err == nil {
		t.Error("expected error or nil CRL for unknown issuer")
	}

	t.Logf("GenerateDERCRL correctly handles unknown issuer")
}

func TestGetOCSPResponse_Good(t *testing.T) {
	svc, certRepo, _, _ := newRevocationTestService()

	// Add a non-revoked certificate
	cert := &domain.ManagedCertificate{
		ID:         "cert-ocsp-good",
		CommonName: "good.example.com",
		IssuerID:   "iss-local",
		Status:     domain.CertificateStatusActive,
		ExpiresAt:  time.Now().AddDate(1, 0, 0),
	}
	certRepo.AddCert(cert)

	version := &domain.CertificateVersion{
		ID:            "ver-ocsp-good",
		CertificateID: "cert-ocsp-good",
		SerialNumber:  "OCSP-GOOD-001",
		NotBefore:     time.Now(),
		NotAfter:      time.Now().AddDate(1, 0, 0),
		CreatedAt:     time.Now(),
	}
	certRepo.Versions["cert-ocsp-good"] = []*domain.CertificateVersion{version}

	// Request OCSP response for good cert
	resp, err := svc.GetOCSPResponse("iss-local", "OCSP-GOOD-001")

	if err != nil {
		t.Fatalf("expected no error, got: %v", err)
	}

	if resp == nil || len(resp) == 0 {
		t.Fatal("expected non-empty OCSP response for good cert")
	}

	t.Logf("OCSP response for good cert generated: %d bytes", len(resp))
}

func TestGetOCSPResponse_Revoked(t *testing.T) {
	svc, certRepo, revocationRepo, _ := newRevocationTestService()

	now := time.Now()

	// Add a revoked certificate
	cert := &domain.ManagedCertificate{
		ID:               "cert-ocsp-revoked",
		CommonName:       "revoked.example.com",
		IssuerID:         "iss-local",
		Status:           domain.CertificateStatusRevoked,
		RevokedAt:        &now,
		RevocationReason: "keyCompromise",
		ExpiresAt:        time.Now().AddDate(1, 0, 0),
	}
	certRepo.AddCert(cert)

	version := &domain.CertificateVersion{
		ID:            "ver-ocsp-revoked",
		CertificateID: "cert-ocsp-revoked",
		SerialNumber:  "OCSP-REVOKED-001",
		NotBefore:     time.Now().Add(-24 * time.Hour),
		NotAfter:      time.Now().AddDate(1, 0, 0),
		CreatedAt:     time.Now(),
	}
	certRepo.Versions["cert-ocsp-revoked"] = []*domain.CertificateVersion{version}

	// Add revocation record
	revocationRepo.Revocations = []*domain.CertificateRevocation{
		{
			SerialNumber:  "OCSP-REVOKED-001",
			CertificateID: "cert-ocsp-revoked",
			IssuerID:      "iss-local",
			Reason:        "keyCompromise",
			RevokedAt:     now.Add(-24 * time.Hour),
			RevokedBy:     "admin",
		},
	}

	// Request OCSP response for revoked cert
	resp, err := svc.GetOCSPResponse("iss-local", "OCSP-REVOKED-001")

	if err != nil {
		t.Fatalf("expected no error, got: %v", err)
	}

	if resp == nil || len(resp) == 0 {
		t.Fatal("expected non-empty OCSP response for revoked cert")
	}

	t.Logf("OCSP response for revoked cert generated: %d bytes", len(resp))
}

func TestGetOCSPResponse_Unknown(t *testing.T) {
	svc, _, _, _ := newRevocationTestService()

	// Request OCSP response for unknown cert
	resp, err := svc.GetOCSPResponse("iss-local", "UNKNOWN-SERIAL")

	if err != nil {
		t.Fatalf("expected no error (should return unknown status), got: %v", err)
	}

	// Response should indicate unknown status
	if resp == nil || len(resp) == 0 {
		t.Fatal("expected non-empty OCSP response even for unknown cert")
	}

	t.Logf("OCSP response for unknown cert generated: %d bytes", len(resp))
}

func TestGetOCSPResponse_IssuerNotFound(t *testing.T) {
	svc, _, _, _ := newRevocationTestService()

	// Request OCSP response for unknown issuer
	resp, err := svc.GetOCSPResponse("iss-unknown", "SOME-SERIAL")

	// Should return error since issuer doesn't exist
	if err == nil && resp != nil {
		t.Error("expected error for unknown issuer")
	}

	t.Logf("GetOCSPResponse correctly handles unknown issuer")
}

func TestGetOCSPResponse_InvalidSerial(t *testing.T) {
	svc, _, _, _ := newRevocationTestService()

	// Request OCSP response with invalid serial format
	resp, err := svc.GetOCSPResponse("iss-local", "")

	if err == nil && resp != nil {
		// Empty serial might return unknown status; that's ok
		t.Logf("Empty serial handled gracefully")
	} else if err != nil {
		t.Logf("Empty serial rejected with error: %v", err)
	}
}
