package local_test

import (
	"context"
	"crypto/ecdsa"
	"crypto/ed25519"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/json"
	"encoding/pem"
	"log/slog"
	"math/big"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/certctl-io/certctl/internal/connector/issuer"
	"github.com/certctl-io/certctl/internal/connector/issuer/local"
)

func TestLocalConnector(t *testing.T) {
	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
	ctx := context.Background()

	// Test 1: Create connector and validate config
	t.Run("ValidateConfig", func(t *testing.T) {
		config := &local.Config{
			CACommonName: "Test CA",
			ValidityDays: 30,
		}
		connector := local.New(config, logger)

		rawConfig, _ := json.Marshal(config)
		err := connector.ValidateConfig(ctx, rawConfig)
		if err != nil {
			t.Fatalf("ValidateConfig failed: %v", err)
		}
	})

	// Test 2: Issue a certificate
	t.Run("IssueCertificate", func(t *testing.T) {
		config := &local.Config{
			CACommonName: "Test CA",
			ValidityDays: 30,
		}
		connector := local.New(config, logger)

		csr, csrPEM, err := generateTestCSR("test.example.com")
		if err != nil {
			t.Fatalf("Failed to generate CSR: %v", err)
		}

		req := issuer.IssuanceRequest{
			CommonName: csr.Subject.CommonName,
			SANs:       []string{"www.test.example.com"},
			CSRPEM:     csrPEM,
		}

		result, err := connector.IssueCertificate(ctx, req)
		if err != nil {
			t.Fatalf("IssueCertificate failed: %v", err)
		}

		if result.Serial == "" {
			t.Error("Serial is empty")
		}
		if result.CertPEM == "" {
			t.Error("CertPEM is empty")
		}
		if result.ChainPEM == "" {
			t.Error("ChainPEM is empty")
		}
		if result.OrderID == "" {
			t.Error("OrderID is empty")
		}
		if result.NotAfter.IsZero() {
			t.Error("NotAfter is zero")
		}

		t.Logf("Certificate issued: serial=%s, orderID=%s", result.Serial, result.OrderID)
	})

	// Test 3: Renew a certificate
	t.Run("RenewCertificate", func(t *testing.T) {
		config := &local.Config{
			CACommonName: "Test CA",
			ValidityDays: 30,
		}
		connector := local.New(config, logger)

		csr, csrPEM, err := generateTestCSR("test.example.com")
		if err != nil {
			t.Fatalf("Failed to generate CSR: %v", err)
		}

		renewReq := issuer.RenewalRequest{
			CommonName: csr.Subject.CommonName,
			SANs:       []string{"www.test.example.com"},
			CSRPEM:     csrPEM,
		}

		result, err := connector.RenewCertificate(ctx, renewReq)
		if err != nil {
			t.Fatalf("RenewCertificate failed: %v", err)
		}

		if result.Serial == "" {
			t.Error("Serial is empty")
		}

		t.Logf("Certificate renewed: serial=%s", result.Serial)
	})

	// Test 4: Get order status
	t.Run("GetOrderStatus", func(t *testing.T) {
		config := &local.Config{
			CACommonName: "Test CA",
			ValidityDays: 30,
		}
		connector := local.New(config, logger)

		status, err := connector.GetOrderStatus(ctx, "local-12345")
		if err != nil {
			t.Fatalf("GetOrderStatus failed: %v", err)
		}

		if status.Status != "completed" {
			t.Errorf("Expected status 'completed', got '%s'", status.Status)
		}

		t.Logf("Order status: %s", status.Status)
	})

	// Test 5: Revoke a certificate
	t.Run("RevokeCertificate", func(t *testing.T) {
		config := &local.Config{
			CACommonName: "Test CA",
			ValidityDays: 30,
		}
		connector := local.New(config, logger)

		revokeReq := issuer.RevocationRequest{
			Serial: "test-serial-12345",
		}

		err := connector.RevokeCertificate(ctx, revokeReq)
		if err != nil {
			t.Fatalf("RevokeCertificate failed: %v", err)
		}

		t.Logf("Certificate revoked: serial=%s", revokeReq.Serial)
	})

	// Test 6: Invalid CSR
	t.Run("InvalidCSR", func(t *testing.T) {
		config := &local.Config{
			CACommonName: "Test CA",
			ValidityDays: 30,
		}
		connector := local.New(config, logger)

		req := issuer.IssuanceRequest{
			CommonName: "test.example.com",
			CSRPEM:     "invalid pem",
		}

		_, err := connector.IssueCertificate(ctx, req)
		if err == nil {
			t.Fatal("Expected error for invalid CSR")
		}

		t.Logf("Correctly rejected invalid CSR: %v", err)
	})
}

// Sub-CA mode tests

func TestSubCAMode(t *testing.T) {
	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
	ctx := context.Background()

	t.Run("SubCA_RSA_IssueCertificate", func(t *testing.T) {
		certPath, keyPath := generateTestSubCA(t, "rsa")
		defer os.Remove(certPath)
		defer os.Remove(keyPath)

		config := &local.Config{
			ValidityDays: 30,
			CACertPath:   certPath,
			CAKeyPath:    keyPath,
		}
		connector := local.New(config, logger)

		_, csrPEM, err := generateTestCSR("app.internal.corp")
		if err != nil {
			t.Fatalf("Failed to generate CSR: %v", err)
		}

		req := issuer.IssuanceRequest{
			CommonName: "app.internal.corp",
			SANs:       []string{"app.internal.corp"},
			CSRPEM:     csrPEM,
		}

		result, err := connector.IssueCertificate(ctx, req)
		if err != nil {
			t.Fatalf("SubCA IssueCertificate failed: %v", err)
		}

		if result.CertPEM == "" {
			t.Error("CertPEM is empty")
		}
		if result.ChainPEM == "" {
			t.Error("ChainPEM is empty (should contain sub-CA cert)")
		}
		if result.Serial == "" {
			t.Error("Serial is empty")
		}

		// Verify the issued cert is signed by the sub-CA (not self-signed)
		certBlock, _ := pem.Decode([]byte(result.CertPEM))
		if certBlock == nil {
			t.Fatal("Failed to decode issued cert PEM")
		}
		cert, err := x509.ParseCertificate(certBlock.Bytes)
		if err != nil {
			t.Fatalf("Failed to parse issued cert: %v", err)
		}

		// The issuer should be the sub-CA, not the cert itself
		if cert.Issuer.CommonName == cert.Subject.CommonName {
			t.Error("Issued cert appears to be self-signed (issuer == subject)")
		}

		t.Logf("Sub-CA issued cert: serial=%s, issuer=%s, subject=%s",
			result.Serial, cert.Issuer.CommonName, cert.Subject.CommonName)
	})

	t.Run("SubCA_ECDSA_IssueCertificate", func(t *testing.T) {
		certPath, keyPath := generateTestSubCA(t, "ecdsa")
		defer os.Remove(certPath)
		defer os.Remove(keyPath)

		config := &local.Config{
			ValidityDays: 30,
			CACertPath:   certPath,
			CAKeyPath:    keyPath,
		}
		connector := local.New(config, logger)

		_, csrPEM, err := generateTestCSR("api.internal.corp")
		if err != nil {
			t.Fatalf("Failed to generate CSR: %v", err)
		}

		req := issuer.IssuanceRequest{
			CommonName: "api.internal.corp",
			SANs:       []string{"api.internal.corp"},
			CSRPEM:     csrPEM,
		}

		result, err := connector.IssueCertificate(ctx, req)
		if err != nil {
			t.Fatalf("SubCA ECDSA IssueCertificate failed: %v", err)
		}

		if result.CertPEM == "" {
			t.Error("CertPEM is empty")
		}

		t.Logf("Sub-CA (ECDSA) issued cert: serial=%s", result.Serial)
	})

	t.Run("SubCA_ValidateConfig_MissingKeyPath", func(t *testing.T) {
		cfg := local.Config{
			ValidityDays: 30,
			CACertPath:   "/some/cert.pem",
			// CAKeyPath intentionally omitted
		}
		connector := local.New(nil, logger)

		rawConfig, _ := json.Marshal(cfg)
		err := connector.ValidateConfig(ctx, rawConfig)
		if err == nil {
			t.Fatal("Expected error when only CACertPath is set")
		}
		t.Logf("Correctly rejected partial sub-CA config: %v", err)
	})

	t.Run("SubCA_ValidateConfig_NonexistentPaths", func(t *testing.T) {
		cfg := local.Config{
			ValidityDays: 30,
			CACertPath:   "/nonexistent/ca.pem",
			CAKeyPath:    "/nonexistent/ca-key.pem",
		}
		connector := local.New(nil, logger)

		rawConfig, _ := json.Marshal(cfg)
		err := connector.ValidateConfig(ctx, rawConfig)
		if err == nil {
			t.Fatal("Expected error for nonexistent file paths")
		}
		t.Logf("Correctly rejected nonexistent paths: %v", err)
	})

	t.Run("SubCA_InvalidCertFile", func(t *testing.T) {
		tmpDir := t.TempDir()
		certPath := filepath.Join(tmpDir, "bad-cert.pem")
		keyPath := filepath.Join(tmpDir, "bad-key.pem")

		// Write garbage data
		os.WriteFile(certPath, []byte("not a certificate"), 0600)
		os.WriteFile(keyPath, []byte("not a key"), 0600)

		config := &local.Config{
			ValidityDays: 30,
			CACertPath:   certPath,
			CAKeyPath:    keyPath,
		}
		connector := local.New(config, logger)

		_, csrPEM, _ := generateTestCSR("test.example.com")
		req := issuer.IssuanceRequest{
			CommonName: "test.example.com",
			CSRPEM:     csrPEM,
		}

		_, err := connector.IssueCertificate(ctx, req)
		if err == nil {
			t.Fatal("Expected error for invalid cert file")
		}
		t.Logf("Correctly rejected invalid cert file: %v", err)
	})

	t.Run("SubCA_NonCACert", func(t *testing.T) {
		// Create a cert that is NOT a CA (no BasicConstraints.IsCA)
		tmpDir := t.TempDir()
		certPath, keyPath := generateTestNonCACert(t, tmpDir)

		config := &local.Config{
			ValidityDays: 30,
			CACertPath:   certPath,
			CAKeyPath:    keyPath,
		}
		connector := local.New(config, logger)

		_, csrPEM, _ := generateTestCSR("test.example.com")
		req := issuer.IssuanceRequest{
			CommonName: "test.example.com",
			CSRPEM:     csrPEM,
		}

		_, err := connector.IssueCertificate(ctx, req)
		if err == nil {
			t.Fatal("Expected error for non-CA cert")
		}
		t.Logf("Correctly rejected non-CA cert: %v", err)
	})

	t.Run("SubCA_ExpiredCert_IsRejected", func(t *testing.T) {
		// Sub-CA expired 1 hour ago. M-5: loadCAFromDisk must fail closed
		// instead of minting child certs that immediately fail path validation
		// at every relying party (CWE-672).
		notBefore := time.Now().AddDate(-1, 0, 0)
		notAfter := time.Now().Add(-1 * time.Hour)
		certPath, keyPath := generateTestSubCAWithValidity(t, "rsa", notBefore, notAfter)

		config := &local.Config{
			ValidityDays: 30,
			CACertPath:   certPath,
			CAKeyPath:    keyPath,
		}
		connector := local.New(config, logger)

		_, csrPEM, err := generateTestCSR("app.internal.corp")
		if err != nil {
			t.Fatalf("Failed to generate CSR: %v", err)
		}
		req := issuer.IssuanceRequest{
			CommonName: "app.internal.corp",
			CSRPEM:     csrPEM,
		}

		_, err = connector.IssueCertificate(ctx, req)
		if err == nil {
			t.Fatal("Expected error when loading expired sub-CA; got nil")
		}
		if !strings.Contains(err.Error(), "expired") {
			t.Errorf("Expected error to mention 'expired'; got: %v", err)
		}
		if !strings.Contains(err.Error(), "Test Sub-CA") {
			t.Errorf("Expected error to include CA subject CN 'Test Sub-CA'; got: %v", err)
		}
		t.Logf("Correctly rejected expired sub-CA: %v", err)
	})

	t.Run("SubCA_NotYetValid_IsRejected", func(t *testing.T) {
		// Sub-CA is not valid for another hour (clock skew or operator error
		// pushing a pre-production CA into prod). M-5: loadCAFromDisk must
		// fail closed.
		notBefore := time.Now().Add(1 * time.Hour)
		notAfter := time.Now().AddDate(5, 0, 0)
		certPath, keyPath := generateTestSubCAWithValidity(t, "rsa", notBefore, notAfter)

		config := &local.Config{
			ValidityDays: 30,
			CACertPath:   certPath,
			CAKeyPath:    keyPath,
		}
		connector := local.New(config, logger)

		_, csrPEM, err := generateTestCSR("app.internal.corp")
		if err != nil {
			t.Fatalf("Failed to generate CSR: %v", err)
		}
		req := issuer.IssuanceRequest{
			CommonName: "app.internal.corp",
			CSRPEM:     csrPEM,
		}

		_, err = connector.IssueCertificate(ctx, req)
		if err == nil {
			t.Fatal("Expected error when loading not-yet-valid sub-CA; got nil")
		}
		if !strings.Contains(err.Error(), "not yet valid") {
			t.Errorf("Expected error to mention 'not yet valid'; got: %v", err)
		}
		if !strings.Contains(err.Error(), "Test Sub-CA") {
			t.Errorf("Expected error to include CA subject CN 'Test Sub-CA'; got: %v", err)
		}
		t.Logf("Correctly rejected not-yet-valid sub-CA: %v", err)
	})

	t.Run("SubCA_BarelyValid_IsAccepted", func(t *testing.T) {
		// Sub-CA valid from 1 minute ago to 1 hour from now. Edge case:
		// proves the M-5 window check doesn't over-reject CAs that are
		// legitimately live but close to the boundaries.
		notBefore := time.Now().Add(-1 * time.Minute)
		notAfter := time.Now().Add(1 * time.Hour)
		certPath, keyPath := generateTestSubCAWithValidity(t, "rsa", notBefore, notAfter)

		config := &local.Config{
			ValidityDays: 30,
			CACertPath:   certPath,
			CAKeyPath:    keyPath,
		}
		connector := local.New(config, logger)

		_, csrPEM, err := generateTestCSR("app.internal.corp")
		if err != nil {
			t.Fatalf("Failed to generate CSR: %v", err)
		}
		req := issuer.IssuanceRequest{
			CommonName: "app.internal.corp",
			CSRPEM:     csrPEM,
		}

		result, err := connector.IssueCertificate(ctx, req)
		if err != nil {
			t.Fatalf("Barely-valid sub-CA was wrongly rejected: %v", err)
		}
		if result.CertPEM == "" {
			t.Error("CertPEM is empty")
		}
		t.Logf("Correctly accepted barely-valid sub-CA: serial=%s", result.Serial)
	})

	t.Run("SubCA_RenewCertificate", func(t *testing.T) {
		certPath, keyPath := generateTestSubCA(t, "rsa")
		defer os.Remove(certPath)
		defer os.Remove(keyPath)

		config := &local.Config{
			ValidityDays: 30,
			CACertPath:   certPath,
			CAKeyPath:    keyPath,
		}
		connector := local.New(config, logger)

		_, csrPEM, err := generateTestCSR("renew.internal.corp")
		if err != nil {
			t.Fatalf("Failed to generate CSR: %v", err)
		}

		renewReq := issuer.RenewalRequest{
			CommonName: "renew.internal.corp",
			SANs:       []string{"renew.internal.corp"},
			CSRPEM:     csrPEM,
		}

		result, err := connector.RenewCertificate(ctx, renewReq)
		if err != nil {
			t.Fatalf("SubCA RenewCertificate failed: %v", err)
		}

		if result.Serial == "" {
			t.Error("Serial is empty")
		}
		t.Logf("Sub-CA renewed cert: serial=%s", result.Serial)
	})
}

// generateTestSubCA creates a self-signed CA cert+key pair and writes them to temp files.
// keyType can be "rsa" or "ecdsa". Validity window is [now, now+5y].
func generateTestSubCA(t *testing.T, keyType string) (certPath, keyPath string) {
	t.Helper()
	return generateTestSubCAWithValidity(t, keyType, time.Now(), time.Now().AddDate(5, 0, 0))
}

// generateTestSubCAWithValidity creates a self-signed CA cert+key pair with an
// explicit NotBefore/NotAfter window. Used by M-5 tests that exercise expired
// and not-yet-valid CA rejection in loadCAFromDisk.
func generateTestSubCAWithValidity(t *testing.T, keyType string, notBefore, notAfter time.Time) (certPath, keyPath string) {
	t.Helper()
	tmpDir := t.TempDir()
	certPath = filepath.Join(tmpDir, "ca.pem")
	keyPath = filepath.Join(tmpDir, "ca-key.pem")

	var privKey interface{}
	var pubKey interface{}
	var keyPEM []byte

	switch keyType {
	case "rsa":
		rsaKey, err := rsa.GenerateKey(rand.Reader, 2048)
		if err != nil {
			t.Fatalf("Failed to generate RSA key: %v", err)
		}
		privKey = rsaKey
		pubKey = &rsaKey.PublicKey
		keyPEM = pem.EncodeToMemory(&pem.Block{
			Type:  "RSA PRIVATE KEY",
			Bytes: x509.MarshalPKCS1PrivateKey(rsaKey),
		})
	case "ecdsa":
		ecKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
		if err != nil {
			t.Fatalf("Failed to generate ECDSA key: %v", err)
		}
		privKey = ecKey
		pubKey = &ecKey.PublicKey
		ecKeyBytes, err := x509.MarshalECPrivateKey(ecKey)
		if err != nil {
			t.Fatalf("Failed to marshal ECDSA key: %v", err)
		}
		keyPEM = pem.EncodeToMemory(&pem.Block{
			Type:  "EC PRIVATE KEY",
			Bytes: ecKeyBytes,
		})
	default:
		t.Fatalf("Unsupported key type: %s", keyType)
	}

	// Create a CA certificate
	template := &x509.Certificate{
		SerialNumber: big.NewInt(1),
		Subject: pkix.Name{
			CommonName:   "Test Sub-CA",
			Organization: []string{"CertCtl Test"},
		},
		NotBefore:             notBefore,
		NotAfter:              notAfter,
		KeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
		BasicConstraintsValid: true,
		IsCA:                  true,
	}

	certBytes, err := x509.CreateCertificate(rand.Reader, template, template, pubKey, privKey)
	if err != nil {
		t.Fatalf("Failed to create CA cert: %v", err)
	}

	certPEM := pem.EncodeToMemory(&pem.Block{
		Type:  "CERTIFICATE",
		Bytes: certBytes,
	})

	if err := os.WriteFile(certPath, certPEM, 0600); err != nil {
		t.Fatalf("Failed to write CA cert: %v", err)
	}
	if err := os.WriteFile(keyPath, keyPEM, 0600); err != nil {
		t.Fatalf("Failed to write CA key: %v", err)
	}

	return certPath, keyPath
}

// generateTestNonCACert creates a cert+key pair where IsCA=false (not a CA cert).
func generateTestNonCACert(t *testing.T, tmpDir string) (certPath, keyPath string) {
	t.Helper()
	certPath = filepath.Join(tmpDir, "not-ca.pem")
	keyPath = filepath.Join(tmpDir, "not-ca-key.pem")

	rsaKey, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		t.Fatalf("Failed to generate RSA key: %v", err)
	}

	template := &x509.Certificate{
		SerialNumber: big.NewInt(1),
		Subject: pkix.Name{
			CommonName: "Not A CA",
		},
		NotBefore:             time.Now(),
		NotAfter:              time.Now().AddDate(1, 0, 0),
		KeyUsage:              x509.KeyUsageDigitalSignature,
		BasicConstraintsValid: true,
		IsCA:                  false, // NOT a CA
	}

	certBytes, err := x509.CreateCertificate(rand.Reader, template, template, &rsaKey.PublicKey, rsaKey)
	if err != nil {
		t.Fatalf("Failed to create non-CA cert: %v", err)
	}

	certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certBytes})
	keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(rsaKey)})

	os.WriteFile(certPath, certPEM, 0600)
	os.WriteFile(keyPath, keyPEM, 0600)

	return certPath, keyPath
}

func generateTestCSR(commonName string) (*x509.CertificateRequest, string, error) {
	key, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		return nil, "", err
	}

	subj := pkix.Name{
		CommonName: commonName,
	}

	csrTemplate := x509.CertificateRequest{
		Subject:            subj,
		DNSNames:           []string{commonName},
		SignatureAlgorithm: x509.SHA256WithRSA,
	}

	csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &csrTemplate, key)
	if err != nil {
		return nil, "", err
	}

	csr, err := x509.ParseCertificateRequest(csrBytes)
	if err != nil {
		return nil, "", err
	}

	csrPEM := pem.EncodeToMemory(&pem.Block{
		Type:  "CERTIFICATE REQUEST",
		Bytes: csrBytes,
	})

	return csr, string(csrPEM), nil
}

// M15b: CRL and OCSP Tests

func TestGenerateCRL_Empty(t *testing.T) {
	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
	ctx := context.Background()

	config := &local.Config{
		CACommonName: "Test CA",
		ValidityDays: 30,
	}
	connector := local.New(config, logger)

	// Generate CRL with no revoked certs — should succeed with 0 entries
	crl, err := connector.GenerateCRL(ctx, nil)
	if err != nil {
		t.Fatalf("GenerateCRL failed: %v", err)
	}

	if crl == nil {
		t.Fatal("CRL is nil")
	}

	// Verify it's valid DER by parsing
	parsedCRL, err := x509.ParseRevocationList(crl)
	if err != nil {
		t.Fatalf("failed to parse CRL: %v", err)
	}

	if len(parsedCRL.RevokedCertificateEntries) != 0 {
		t.Errorf("expected 0 revoked entries, got %d", len(parsedCRL.RevokedCertificateEntries))
	}

	t.Logf("Empty CRL generated successfully with %d entries", len(parsedCRL.RevokedCertificateEntries))
}

func TestGenerateCRL_WithEntries(t *testing.T) {
	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
	ctx := context.Background()

	config := &local.Config{
		CACommonName: "Test CA",
		ValidityDays: 30,
	}
	connector := local.New(config, logger)

	// Generate CRL with 2 revoked certs
	entries := []issuer.RevokedCertEntry{
		{SerialNumber: big.NewInt(12345), RevokedAt: time.Now().Add(-24 * time.Hour), ReasonCode: 1},
		{SerialNumber: big.NewInt(67890), RevokedAt: time.Now().Add(-1 * time.Hour), ReasonCode: 4},
	}

	crl, err := connector.GenerateCRL(ctx, entries)
	if err != nil {
		t.Fatalf("GenerateCRL failed: %v", err)
	}

	if crl == nil {
		t.Fatal("CRL is nil")
	}

	parsedCRL, err := x509.ParseRevocationList(crl)
	if err != nil {
		t.Fatalf("failed to parse CRL: %v", err)
	}

	if len(parsedCRL.RevokedCertificateEntries) != 2 {
		t.Errorf("expected 2 revoked entries, got %d", len(parsedCRL.RevokedCertificateEntries))
	}

	// Verify entries contain expected serials
	serials := make(map[string]bool)
	for _, entry := range parsedCRL.RevokedCertificateEntries {
		serials[entry.SerialNumber.String()] = true
	}

	if !serials["12345"] {
		t.Error("expected serial 12345 in CRL")
	}
	if !serials["67890"] {
		t.Error("expected serial 67890 in CRL")
	}

	t.Logf("CRL with entries generated successfully: %d entries", len(parsedCRL.RevokedCertificateEntries))
}

func TestGenerateCRL_BeforeCAInit(t *testing.T) {
	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
	ctx := context.Background()

	// CRL generation should init the CA automatically
	cfg := &local.Config{ValidityDays: 90}
	connector := local.New(cfg, logger)

	crl, err := connector.GenerateCRL(ctx, nil)
	if err != nil {
		t.Fatalf("GenerateCRL failed: %v", err)
	}

	if crl == nil {
		t.Fatal("CRL is nil")
	}

	// Verify it's valid
	_, err = x509.ParseRevocationList(crl)
	if err != nil {
		t.Fatalf("failed to parse CRL: %v", err)
	}

	t.Log("CRL generated with auto-initialized CA")
}

func TestGenerateCRL_WithReasonCodes(t *testing.T) {
	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
	ctx := context.Background()

	config := &local.Config{
		CACommonName: "Test CA",
		ValidityDays: 30,
	}
	connector := local.New(config, logger)

	// Test all RFC 5280 reason codes
	entries := []issuer.RevokedCertEntry{
		{SerialNumber: big.NewInt(100), RevokedAt: time.Now(), ReasonCode: 0}, // unspecified
		{SerialNumber: big.NewInt(101), RevokedAt: time.Now(), ReasonCode: 1}, // keyCompromise
		{SerialNumber: big.NewInt(102), RevokedAt: time.Now(), ReasonCode: 2}, // caCompromise
		{SerialNumber: big.NewInt(103), RevokedAt: time.Now(), ReasonCode: 3}, // affiliationChanged
		{SerialNumber: big.NewInt(104), RevokedAt: time.Now(), ReasonCode: 4}, // superseded
	}

	crl, err := connector.GenerateCRL(ctx, entries)
	if err != nil {
		t.Fatalf("GenerateCRL failed: %v", err)
	}

	parsedCRL, err := x509.ParseRevocationList(crl)
	if err != nil {
		t.Fatalf("failed to parse CRL: %v", err)
	}

	if len(parsedCRL.RevokedCertificateEntries) != 5 {
		t.Errorf("expected 5 revoked entries, got %d", len(parsedCRL.RevokedCertificateEntries))
	}

	// Verify reason codes are preserved
	reasonCount := 0
	for _, entry := range parsedCRL.RevokedCertificateEntries {
		if entry.ReasonCode >= 0 {
			reasonCount++
		}
	}
	if reasonCount != 5 {
		t.Errorf("expected all 5 entries to have reason codes, got %d", reasonCount)
	}

	t.Logf("CRL with %d reason codes generated successfully", reasonCount)
}

func TestSignOCSPResponse_Good(t *testing.T) {
	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
	ctx := context.Background()

	config := &local.Config{
		CACommonName: "Test CA",
		ValidityDays: 30,
	}
	connector := local.New(config, logger)

	now := time.Now()
	resp, err := connector.SignOCSPResponse(ctx, issuer.OCSPSignRequest{
		CertSerial: big.NewInt(12345),
		CertStatus: 0, // good
		ThisUpdate: now,
		NextUpdate: now.Add(1 * time.Hour),
	})

	if err != nil {
		t.Fatalf("SignOCSPResponse failed: %v", err)
	}

	if resp == nil {
		t.Fatal("OCSP response is nil")
	}

	if len(resp) == 0 {
		t.Fatal("OCSP response is empty")
	}

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

func TestSignOCSPResponse_Revoked(t *testing.T) {
	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
	ctx := context.Background()

	config := &local.Config{
		CACommonName: "Test CA",
		ValidityDays: 30,
	}
	connector := local.New(config, logger)

	now := time.Now()
	revokedAt := now.Add(-24 * time.Hour)

	resp, err := connector.SignOCSPResponse(ctx, issuer.OCSPSignRequest{
		CertSerial:       big.NewInt(12345),
		CertStatus:       1, // revoked
		RevokedAt:        revokedAt,
		RevocationReason: 1, // keyCompromise
		ThisUpdate:       now,
		NextUpdate:       now.Add(1 * time.Hour),
	})

	if err != nil {
		t.Fatalf("SignOCSPResponse failed: %v", err)
	}

	if resp == nil {
		t.Fatal("OCSP response is nil")
	}

	if len(resp) == 0 {
		t.Fatal("OCSP response is empty")
	}

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

func TestSignOCSPResponse_Unknown(t *testing.T) {
	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
	ctx := context.Background()

	config := &local.Config{
		CACommonName: "Test CA",
		ValidityDays: 30,
	}
	connector := local.New(config, logger)

	now := time.Now()
	resp, err := connector.SignOCSPResponse(ctx, issuer.OCSPSignRequest{
		CertSerial: big.NewInt(12345),
		CertStatus: 2, // unknown
		ThisUpdate: now,
		NextUpdate: now.Add(1 * time.Hour),
	})

	if err != nil {
		t.Fatalf("SignOCSPResponse failed: %v", err)
	}

	if resp == nil {
		t.Fatal("OCSP response is nil")
	}

	if len(resp) == 0 {
		t.Fatal("OCSP response is empty")
	}

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

func TestSignOCSPResponse_BeforeCAInit(t *testing.T) {
	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
	ctx := context.Background()

	cfg := &local.Config{ValidityDays: 90}
	connector := local.New(cfg, logger)

	now := time.Now()
	resp, err := connector.SignOCSPResponse(ctx, issuer.OCSPSignRequest{
		CertSerial: big.NewInt(999),
		CertStatus: 0,
		ThisUpdate: now,
		NextUpdate: now.Add(1 * time.Hour),
	})

	if err != nil {
		t.Fatalf("SignOCSPResponse failed: %v", err)
	}

	if resp == nil || len(resp) == 0 {
		t.Fatal("OCSP response is nil or empty")
	}

	t.Log("OCSP response generated with auto-initialized CA")
}

func TestGenerateCRL_SubCA(t *testing.T) {
	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
	ctx := context.Background()

	certPath, keyPath := generateTestSubCA(t, "rsa")
	defer os.Remove(certPath)
	defer os.Remove(keyPath)

	config := &local.Config{
		ValidityDays: 30,
		CACertPath:   certPath,
		CAKeyPath:    keyPath,
	}
	connector := local.New(config, logger)

	entries := []issuer.RevokedCertEntry{
		{SerialNumber: big.NewInt(555), RevokedAt: time.Now().Add(-12 * time.Hour), ReasonCode: 2},
	}

	crl, err := connector.GenerateCRL(ctx, entries)
	if err != nil {
		t.Fatalf("SubCA GenerateCRL failed: %v", err)
	}

	if crl == nil {
		t.Fatal("CRL is nil")
	}

	parsedCRL, err := x509.ParseRevocationList(crl)
	if err != nil {
		t.Fatalf("failed to parse SubCA CRL: %v", err)
	}

	if len(parsedCRL.RevokedCertificateEntries) != 1 {
		t.Errorf("expected 1 entry in SubCA CRL, got %d", len(parsedCRL.RevokedCertificateEntries))
	}

	t.Log("SubCA CRL generated successfully")
}

// M11c: MaxTTL enforcement tests

func TestIssueCertificate_MaxTTL_CapsValidity(t *testing.T) {
	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
	ctx := context.Background()

	config := &local.Config{
		CACommonName: "Test CA",
		ValidityDays: 365, // would normally be 1 year
	}
	connector := local.New(config, logger)

	_, csrPEM, err := generateTestCSR("maxttl.example.com")
	if err != nil {
		t.Fatalf("Failed to generate CSR: %v", err)
	}

	// MaxTTLSeconds = 3600 (1 hour) should cap the 365-day validity
	req := issuer.IssuanceRequest{
		CommonName:    "maxttl.example.com",
		SANs:          []string{"maxttl.example.com"},
		CSRPEM:        csrPEM,
		MaxTTLSeconds: 3600,
	}

	result, err := connector.IssueCertificate(ctx, req)
	if err != nil {
		t.Fatalf("IssueCertificate failed: %v", err)
	}

	// Cert validity should be ~1 hour, not 365 days
	duration := result.NotAfter.Sub(result.NotBefore)
	if duration > 2*time.Hour {
		t.Errorf("expected validity ≤1h, got %v", duration)
	}
	if duration < 30*time.Minute {
		t.Errorf("expected validity ≥30m, got %v (too short)", duration)
	}

	t.Logf("MaxTTL capped: validity=%v (NotBefore=%v, NotAfter=%v)", duration, result.NotBefore, result.NotAfter)
}

func TestIssueCertificate_MaxTTL_ZeroMeansNoCap(t *testing.T) {
	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
	ctx := context.Background()

	config := &local.Config{
		CACommonName: "Test CA",
		ValidityDays: 30,
	}
	connector := local.New(config, logger)

	_, csrPEM, err := generateTestCSR("nocap.example.com")
	if err != nil {
		t.Fatalf("Failed to generate CSR: %v", err)
	}

	req := issuer.IssuanceRequest{
		CommonName:    "nocap.example.com",
		SANs:          []string{"nocap.example.com"},
		CSRPEM:        csrPEM,
		MaxTTLSeconds: 0, // no cap
	}

	result, err := connector.IssueCertificate(ctx, req)
	if err != nil {
		t.Fatalf("IssueCertificate failed: %v", err)
	}

	// Should get ~30 days as configured
	duration := result.NotAfter.Sub(result.NotBefore)
	if duration < 29*24*time.Hour {
		t.Errorf("expected ~30 day validity without MaxTTL cap, got %v", duration)
	}

	t.Logf("No MaxTTL cap: validity=%v", duration)
}

func TestIssueCertificate_MaxTTL_LargerThanValidityDays_NoCap(t *testing.T) {
	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
	ctx := context.Background()

	config := &local.Config{
		CACommonName: "Test CA",
		ValidityDays: 30,
	}
	connector := local.New(config, logger)

	_, csrPEM, err := generateTestCSR("larger.example.com")
	if err != nil {
		t.Fatalf("Failed to generate CSR: %v", err)
	}

	// MaxTTL = 365 days, but ValidityDays = 30. The shorter one wins.
	req := issuer.IssuanceRequest{
		CommonName:    "larger.example.com",
		SANs:          []string{"larger.example.com"},
		CSRPEM:        csrPEM,
		MaxTTLSeconds: 365 * 24 * 3600, // 365 days
	}

	result, err := connector.IssueCertificate(ctx, req)
	if err != nil {
		t.Fatalf("IssueCertificate failed: %v", err)
	}

	// Should still be ~30 days (ValidityDays wins when shorter)
	duration := result.NotAfter.Sub(result.NotBefore)
	if duration > 31*24*time.Hour {
		t.Errorf("expected ~30 day validity (ValidityDays wins), got %v", duration)
	}

	t.Logf("MaxTTL larger than ValidityDays: validity=%v (ValidityDays wins)", duration)
}

func TestRenewCertificate_MaxTTL_CapsValidity(t *testing.T) {
	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
	ctx := context.Background()

	config := &local.Config{
		CACommonName: "Test CA",
		ValidityDays: 365,
	}
	connector := local.New(config, logger)

	_, csrPEM, err := generateTestCSR("renew-maxttl.example.com")
	if err != nil {
		t.Fatalf("Failed to generate CSR: %v", err)
	}

	req := issuer.RenewalRequest{
		CommonName:    "renew-maxttl.example.com",
		SANs:          []string{"renew-maxttl.example.com"},
		CSRPEM:        csrPEM,
		MaxTTLSeconds: 7200, // 2 hours
	}

	result, err := connector.RenewCertificate(ctx, req)
	if err != nil {
		t.Fatalf("RenewCertificate failed: %v", err)
	}

	duration := result.NotAfter.Sub(result.NotBefore)
	if duration > 3*time.Hour {
		t.Errorf("expected validity ≤2h for renewal MaxTTL, got %v", duration)
	}

	t.Logf("Renewal MaxTTL capped: validity=%v", duration)
}

func TestSignOCSPResponse_SubCA(t *testing.T) {
	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
	ctx := context.Background()

	certPath, keyPath := generateTestSubCA(t, "ecdsa")
	defer os.Remove(certPath)
	defer os.Remove(keyPath)

	config := &local.Config{
		ValidityDays: 30,
		CACertPath:   certPath,
		CAKeyPath:    keyPath,
	}
	connector := local.New(config, logger)

	now := time.Now()
	resp, err := connector.SignOCSPResponse(ctx, issuer.OCSPSignRequest{
		CertSerial: big.NewInt(777),
		CertStatus: 0,
		ThisUpdate: now,
		NextUpdate: now.Add(1 * time.Hour),
	})

	if err != nil {
		t.Fatalf("SubCA SignOCSPResponse failed: %v", err)
	}

	if resp == nil || len(resp) == 0 {
		t.Fatal("SubCA OCSP response is nil or empty")
	}

	t.Log("SubCA OCSP response generated successfully")
}

// TestSubCA_LoadCAFromDisk_RejectsUnsupportedKeyAlgorithm pins the new
// signer.Wrap error path introduced when local.go was refactored to
// route every CA-signing call through the Signer interface. The
// historical parsePrivateKey accepted any PKCS#8 key that satisfied
// crypto.Signer (including Ed25519). The new flow keeps that
// parse-time acceptance but adds a Wrap step that enforces the
// certctl-supported algorithm enum (RSA-2048/3072/4096, ECDSA-P256/P384).
//
// This test confirms an Ed25519 sub-CA key fails LOUDLY at load time
// with a clear "wrap CA private key as signer" error — instead of
// either crashing later at sign time or silently producing a cert
// chain certctl cannot revalidate. Pins both:
//   - the new error path coverage (recovers the 0.5pp drop introduced
//     by the parsePrivateKey deletion)
//   - the contract that loaded sub-CA keys MUST be in the supported
//     algorithm enum
func TestSubCA_LoadCAFromDisk_RejectsUnsupportedKeyAlgorithm(t *testing.T) {
	logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
	ctx := context.Background()
	tmpDir := t.TempDir()

	// Build a valid CA cert signed by RSA so cert-validation passes...
	rsaKey, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		t.Fatalf("rsa keygen: %v", err)
	}
	caTemplate := &x509.Certificate{
		SerialNumber:          big.NewInt(42),
		Subject:               pkix.Name{CommonName: "Mismatched-Key Test CA"},
		NotBefore:             time.Now().Add(-time.Hour),
		NotAfter:              time.Now().Add(24 * time.Hour),
		KeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
		IsCA:                  true,
		BasicConstraintsValid: true,
	}
	certDER, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, &rsaKey.PublicKey, rsaKey)
	if err != nil {
		t.Fatalf("create cert: %v", err)
	}
	certPath := filepath.Join(tmpDir, "ca.crt")
	if err := os.WriteFile(certPath, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}), 0o600); err != nil {
		t.Fatalf("write cert: %v", err)
	}

	// ...but write an UNRELATED Ed25519 key to disk. The Connector's
	// loadCAFromDisk does not enforce key-cert key match — it only
	// validates the cert and parses the key. The newly-introduced
	// signer.Wrap step is what rejects Ed25519.
	_, edPriv, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		t.Fatalf("ed25519 keygen: %v", err)
	}
	edDER, err := x509.MarshalPKCS8PrivateKey(edPriv)
	if err != nil {
		t.Fatalf("marshal ed25519 PKCS#8: %v", err)
	}
	keyPath := filepath.Join(tmpDir, "ca.key")
	if err := os.WriteFile(keyPath, pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: edDER}), 0o600); err != nil {
		t.Fatalf("write key: %v", err)
	}

	conn := local.New(&local.Config{
		CACommonName: "Mismatched-Key Test CA",
		ValidityDays: 90,
		CACertPath:   certPath,
		CAKeyPath:    keyPath,
	}, logger)

	// IssueCertificate triggers ensureCA → loadCAFromDisk → ParsePrivateKey
	// (succeeds for Ed25519 PKCS#8) → signer.Wrap (rejects Ed25519).
	dummyKey, _ := rsa.GenerateKey(rand.Reader, 2048)
	csrTpl := &x509.CertificateRequest{Subject: pkix.Name{CommonName: "leaf.example.com"}}
	csrDER, _ := x509.CreateCertificateRequest(rand.Reader, csrTpl, dummyKey)
	csrPEM := string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrDER}))

	_, err = conn.IssueCertificate(ctx, issuer.IssuanceRequest{
		CommonName: "leaf.example.com",
		CSRPEM:     csrPEM,
	})
	if err == nil {
		t.Fatal("expected IssueCertificate to fail for Ed25519 sub-CA key, got nil")
	}
	if !strings.Contains(err.Error(), "wrap CA private key as signer") {
		t.Fatalf("expected error to mention 'wrap CA private key as signer', got: %v", err)
	}
}
