package security

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/pem"
	"os"
	"path/filepath"
	"testing"
	"time"
)

// TestRoundTrip exercises the full CA → CSR → SignCSR → x509.Verify chain
// in a single test. The point is to catch protocol mismatches early: if
// SignCSR produces a cert that doesn't chain to the CA, the verification
// step will fail and this test will surface the bug.
func TestRoundTrip(t *testing.T) {
	dir := t.TempDir()

	// 1. Init a CA.
	ca, err := CAInit(dir, "orca-test-ca")
	if err != nil {
		t.Fatalf("CAInit: %v", err)
	}
	if ca == nil || ca.Cert == nil {
		t.Fatal("CAInit returned nil cert")
	}
	if !ca.Cert.IsCA {
		t.Error("CA cert IsCA is false")
	}
	if got := ca.Cert.KeyUsage & x509.KeyUsageCertSign; got == 0 {
		t.Error("CA cert missing KeyUsageCertSign")
	}

	// 2. Generate a server CSR with SANs.
	commonName := "test.orca.local"
	sans := []string{"test.orca.local", "127.0.0.1"}
	keyPEM, csrPEM, err := GenerateCSR(commonName, sans)
	if err != nil {
		t.Fatalf("GenerateCSR: %v", err)
	}
	if len(keyPEM) == 0 || len(csrPEM) == 0 {
		t.Fatal("GenerateCSR returned empty PEM")
	}

	// 3. Sign the CSR.
	signedPEM, err := ca.SignCSR(csrPEM)
	if err != nil {
		t.Fatalf("SignCSR: %v", err)
	}
	if len(signedPEM) == 0 {
		t.Fatal("SignCSR returned empty cert")
	}

	// 4. Verify the chain programmatically with x509.Verify.
	caPool := x509.NewCertPool()
	caPool.AddCert(ca.Cert)
	leafBlock, _ := pem.Decode(signedPEM)
	if leafBlock == nil {
		t.Fatal("pem.Decode: no cert block")
	}
	leaf, err := x509.ParseCertificate(leafBlock.Bytes)
	if err != nil {
		t.Fatalf("ParseCertificate (leaf): %v", err)
	}
	_, err = leaf.Verify(x509.VerifyOptions{
		Roots:       caPool,
		KeyUsages:   []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
		CurrentTime: time.Now(),
	})
	if err != nil {
		t.Fatalf("leaf.Verify: %v", err)
	}

	// 5. Sanity-check the SANs survived signing.
	if len(leaf.DNSNames) != 1 || leaf.DNSNames[0] != "test.orca.local" {
		t.Errorf("expected DNS SAN [test.orca.local], got %v", leaf.DNSNames)
	}
	if len(leaf.IPAddresses) != 1 || leaf.IPAddresses[0].String() != "127.0.0.1" {
		t.Errorf("expected IP SAN [127.0.0.1], got %v", leaf.IPAddresses)
	}
	if leaf.Subject.CommonName != commonName {
		t.Errorf("expected CN %q, got %q", commonName, leaf.Subject.CommonName)
	}

	// 6. CA fingerprint pin should match the on-disk ca.crt.
	caFingerprint, err := Fingerprint(filepath.Join(dir, CACertFile))
	if err != nil {
		t.Fatalf("Fingerprint: %v", err)
	}
	if caFingerprint != ca.Fingerprint() {
		t.Errorf("Fingerprint mismatch: file=%q CA.Fingerprint()=%q", caFingerprint, ca.Fingerprint())
	}
	if len(caFingerprint) != 64 {
		t.Errorf("expected 64 hex chars, got %d (%q)", len(caFingerprint), caFingerprint)
	}
}

// TestCAFileModes verifies REQ-033: ca.crt must be 0644, ca.key must be 0600.
func TestCAFileModes(t *testing.T) {
	dir := t.TempDir()
	if _, err := CAInit(dir, "orca-mode-test"); err != nil {
		t.Fatalf("CAInit: %v", err)
	}
	certInfo, err := os.Stat(filepath.Join(dir, CACertFile))
	if err != nil {
		t.Fatalf("stat ca.crt: %v", err)
	}
	keyInfo, err := os.Stat(filepath.Join(dir, CAKeyFile))
	if err != nil {
		t.Fatalf("stat ca.key: %v", err)
	}
	if got := certInfo.Mode().Perm(); got != CACPEMMode {
		t.Errorf("ca.crt mode = %04o, want %04o (REQ-033)", got, CACPEMMode)
	}
	if got := keyInfo.Mode().Perm(); got != CAMode {
		t.Errorf("ca.key mode = %04o, want %04o (REQ-033)", got, CAMode)
	}
}

// TestCAEnforceFileModes verifies that EnforceFileModes refuses to load a CA
// whose file modes are wrong (e.g., ca.key is world-readable).
func TestCAEnforceFileModes(t *testing.T) {
	dir := t.TempDir()
	if _, err := CAInit(dir, "orca-enforce-test"); err != nil {
		t.Fatalf("CAInit: %v", err)
	}
	// Make ca.key world-readable — should fail EnforceFileModes.
	if err := os.Chmod(filepath.Join(dir, CAKeyFile), 0o644); err != nil {
		t.Fatalf("chmod: %v", err)
	}
	if err := EnforceFileModes(dir); err == nil {
		t.Error("expected EnforceFileModes to fail with world-readable ca.key")
	}
	// And LoadCA should refuse too.
	if _, err := LoadCA(dir); err == nil {
		t.Error("expected LoadCA to fail with world-readable ca.key")
	}
	// Restore mode; should pass again.
	if err := os.Chmod(filepath.Join(dir, CAKeyFile), CAMode); err != nil {
		t.Fatalf("chmod restore: %v", err)
	}
	if err := EnforceFileModes(dir); err != nil {
		t.Errorf("EnforceFileModes after restore: %v", err)
	}
}

// TestRotationAlarmFiresAt30Days verifies REQ-034: a cert with NotAfter
// 30 days from now triggers RotationAlarm; a cert with 31 days does not.
func TestRotationAlarmFiresAt30Days(t *testing.T) {
	now := time.Now()
	tests := []struct {
		name      string
		notAfter  time.Time
		wantError bool
	}{
		{
			name:      "31 days remaining",
			notAfter:  now.Add(31 * 24 * time.Hour),
			wantError: false,
		},
		{
			name:      "30 days remaining (boundary, fires)",
			notAfter:  now.Add(30 * 24 * time.Hour),
			wantError: true,
		},
		{
			name:      "15 days remaining (fires)",
			notAfter:  now.Add(15 * 24 * time.Hour),
			wantError: true,
		},
		{
			name:      "expired (fires, days=0)",
			notAfter:  now.Add(-1 * time.Hour),
			wantError: true,
		},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			cert := &x509.Certificate{NotAfter: tc.notAfter}
			err := RotationAlarmAt(cert, now)
			if tc.wantError && err == nil {
				t.Errorf("expected alarm, got nil")
			}
			if !tc.wantError && err != nil {
				t.Errorf("expected no alarm, got %v", err)
			}
		})
	}
}

// TestRedactStripsPrivateKey verifies REQ-035: the Redact helper strips
// PEM private key blocks from arbitrary input.
func TestRedactStripsPrivateKey(t *testing.T) {
	in := []byte(`hello
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAxxxx
-----END RSA PRIVATE KEY-----
world
-----BEGIN CERTIFICATE-----
MIIDazCCAlOgAwIBAgI...
-----END CERTIFICATE-----
trailing
`)
	out := string(Redact(in))
	if contains(out, "PRIVATE KEY-----") {
		t.Errorf("Redact output still contains PRIVATE KEY header: %q", out)
	}
	if contains(out, "BEGIN RSA PRIVATE KEY") {
		t.Errorf("Redact output still contains BEGIN RSA PRIVATE KEY: %q", out)
	}
	if !contains(out, "[REDACTED PRIVATE KEY]") {
		t.Errorf("expected redaction marker in output: %q", out)
	}
	if !contains(out, "BEGIN CERTIFICATE") {
		t.Errorf("expected CERTIFICATE block to survive redaction: %q", out)
	}
	if !contains(out, "hello") || !contains(out, "world") || !contains(out, "trailing") {
		t.Errorf("expected non-key content preserved: %q", out)
	}
}

// TestRedactNoKey verifies Redact is a no-op (other than a copy) when no
// private key blocks are present.
func TestRedactNoKey(t *testing.T) {
	in := []byte("just a cert\n-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n")
	out := string(Redact(in))
	if out != string(in) {
		t.Errorf("Redact changed input without any keys present:\n got=%q\nwant=%q", out, in)
	}
}

// TestGenerateCSRRequiresSANs verifies REQ-036: a CSR without any SANs is
// rejected at generation time.
func TestGenerateCSRRequiresSANs(t *testing.T) {
	if _, _, err := GenerateCSR("foo", nil); err == nil {
		t.Error("expected GenerateCSR to fail with empty sans")
	}
	if _, _, err := GenerateCSR("", []string{"foo"}); err == nil {
		t.Error("expected GenerateCSR to fail with empty commonName")
	}
}

// TestSignCSRRejectsSANless verifies REQ-036: even a syntactically valid CSR
// with no SANs is rejected at sign-time.
func TestSignCSRRejectsSANless(t *testing.T) {
	dir := t.TempDir()
	ca, err := CAInit(dir, "orca-sign-reject-test")
	if err != nil {
		t.Fatalf("CAInit: %v", err)
	}
	// Build a CSR directly with no SANs to bypass the GenerateCSR guard.
	key, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		t.Fatalf("generate key: %v", err)
	}
	csr := &x509.CertificateRequest{
		Subject:  pkix.Name{CommonName: "nosan.example"},
		DNSNames: nil,
	}
	csrDER, err := x509.CreateCertificateRequest(rand.Reader, csr, key)
	if err != nil {
		t.Fatalf("CreateCertificateRequest: %v", err)
	}
	csrPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrDER})
	if _, err := ca.SignCSR(csrPEM); err == nil {
		t.Error("expected SignCSR to fail with SAN-less CSR (REQ-036)")
	}
}

// TestFingerprintStable verifies the SHA-256 hex is identical across two
// computations of the same DER.
func TestFingerprintStable(t *testing.T) {
	dir := t.TempDir()
	if _, err := CAInit(dir, "orca-fp-test"); err != nil {
		t.Fatalf("CAInit: %v", err)
	}
	caPEM, err := os.ReadFile(filepath.Join(dir, CACertFile))
	if err != nil {
		t.Fatalf("read ca.crt: %v", err)
	}
	der, err := firstCertDER(caPEM)
	if err != nil {
		t.Fatalf("firstCertDER: %v", err)
	}
	fp1 := FingerprintOf(der)
	fp2 := FingerprintOf(der)
	if fp1 != fp2 {
		t.Errorf("FingerprintOf not stable: %q vs %q", fp1, fp2)
	}
	if len(fp1) != 64 {
		t.Errorf("expected 64 hex chars, got %d", len(fp1))
	}
}

func contains(haystack, needle string) bool {
	return indexOf(haystack, needle) >= 0
}

func indexOf(s, sub string) int {
	for i := 0; i+len(sub) <= len(s); i++ {
		if s[i:i+len(sub)] == sub {
			return i
		}
	}
	return -1
}
