package main

import (
	"bytes"
	"context"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/rsa"
	"crypto/tls"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/json"
	"encoding/pem"
	"io"
	"log/slog"
	"math/big"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"
)

// TestAgent_Heartbeat_Success tests that heartbeat sends correct metadata and handles 200 response.
func TestAgent_Heartbeat_Success(t *testing.T) {
	// Create mock server to validate heartbeat request
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Verify correct endpoint and method
		if r.URL.Path != "/api/v1/agents/a-test-agent/heartbeat" {
			t.Errorf("unexpected path: %s", r.URL.Path)
		}
		if r.Method != http.MethodPost {
			t.Errorf("unexpected method: %s, expected POST", r.Method)
		}

		// Verify auth header
		auth := r.Header.Get("Authorization")
		if auth != "Bearer test-key" {
			t.Errorf("unexpected auth header: %s", auth)
		}

		// Verify request body contains required fields
		var payload map[string]string
		if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
			t.Fatalf("failed to decode payload: %v", err)
		}

		// Check required fields
		if _, ok := payload["version"]; !ok {
			t.Error("missing version in heartbeat")
		}
		if _, ok := payload["hostname"]; !ok {
			t.Error("missing hostname in heartbeat")
		}
		if _, ok := payload["os"]; !ok {
			t.Error("missing os in heartbeat")
		}
		if _, ok := payload["architecture"]; !ok {
			t.Error("missing architecture in heartbeat")
		}

		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()

	cfg := &AgentConfig{
		ServerURL: server.URL,
		APIKey:    "test-key",
		AgentID:   "a-test-agent",
		Hostname:  "test-host",
	}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	agent, _ := NewAgent(cfg, logger)

	// Should not panic
	agent.sendHeartbeat(context.Background())
}

// TestAgent_Heartbeat_ServerError tests that heartbeat handles 500 response gracefully.
func TestAgent_Heartbeat_ServerError(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusInternalServerError)
		w.Write([]byte("server error"))
	}))
	defer server.Close()

	cfg := &AgentConfig{
		ServerURL: server.URL,
		APIKey:    "test-key",
		AgentID:   "a-test-agent",
		Hostname:  "test-host",
	}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	agent, _ := NewAgent(cfg, logger)

	// Should increment consecutive failures
	failureBefore := agent.consecutiveFailures
	agent.sendHeartbeat(context.Background())
	failureAfter := agent.consecutiveFailures

	if failureAfter != failureBefore+1 {
		t.Errorf("expected consecutive failures to increment, got %d, want %d", failureAfter, failureBefore+1)
	}
}

// TestAgent_Heartbeat_ConnectionError tests that heartbeat handles connection error.
func TestAgent_Heartbeat_ConnectionError(t *testing.T) {
	// Use an invalid address that will fail immediately
	cfg := &AgentConfig{
		ServerURL: "http://invalid-host-that-does-not-exist.local:9999",
		APIKey:    "test-key",
		AgentID:   "a-test-agent",
		Hostname:  "test-host",
	}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	agent, _ := NewAgent(cfg, logger)

	// Should fail due to connection error
	agent.sendHeartbeat(context.Background())

	if agent.consecutiveFailures != 1 {
		t.Errorf("expected consecutive failures to be 1, got %d", agent.consecutiveFailures)
	}
}

// TestAgent_PollWork_NoWork tests that work polling handles empty work list.
func TestAgent_PollWork_NoWork(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/api/v1/agents/a-test-agent/work" {
			t.Errorf("unexpected path: %s", r.URL.Path)
		}
		if r.Method != http.MethodGet {
			t.Errorf("unexpected method: %s", r.Method)
		}

		w.WriteHeader(http.StatusOK)
		json.NewEncoder(w).Encode(WorkResponse{
			Jobs:  []JobItem{},
			Count: 0,
		})
	}))
	defer server.Close()

	cfg := &AgentConfig{
		ServerURL: server.URL,
		APIKey:    "test-key",
		AgentID:   "a-test-agent",
		Hostname:  "test-host",
	}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	agent, _ := NewAgent(cfg, logger)

	// Should not panic
	agent.pollForWork(context.Background())
}

// TestAgent_PollWork_Success tests that work polling parses and returns jobs correctly.
func TestAgent_PollWork_Success(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)

		workResp := WorkResponse{
			Count: 2,
			Jobs: []JobItem{
				{
					ID:            "j-csr-001",
					Type:          "Issuance",
					CertificateID: "mc-001",
					CommonName:    "example.com",
					SANs:          []string{"www.example.com"},
					Status:        "AwaitingCSR",
				},
				{
					ID:            "j-deploy-001",
					Type:          "Deployment",
					CertificateID: "mc-001",
					TargetID:      strPtr("t-nginx-1"),
					TargetType:    "NGINX",
					TargetConfig:  json.RawMessage(`{"cert_path":"/etc/nginx/cert.pem"}`),
					Status:        "Pending",
				},
			},
		}

		json.NewEncoder(w).Encode(workResp)
	}))
	defer server.Close()

	cfg := &AgentConfig{
		ServerURL: server.URL,
		APIKey:    "test-key",
		AgentID:   "a-test-agent",
		Hostname:  "test-host",
	}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	agent, _ := NewAgent(cfg, logger)

	// Should not panic; work items are processed in separate gorines in real usage
	agent.pollForWork(context.Background())
}

// TestSplitPEMChain tests PEM chain splitting into cert and chain.
func TestSplitPEMChain(t *testing.T) {
	// Create two test certificates
	cert1, _ := generateTestCertWithCN("cert1.example.com")
	cert2, _ := generateTestCertWithCN("cert2.example.com")

	block1 := &pem.Block{Type: "CERTIFICATE", Bytes: cert1.Raw}
	block2 := &pem.Block{Type: "CERTIFICATE", Bytes: cert2.Raw}

	cert1PEM := string(pem.EncodeToMemory(block1))
	cert2PEM := string(pem.EncodeToMemory(block2))

	chainPEM := cert1PEM + "\n" + cert2PEM

	// Split
	certOnly, chain := splitPEMChain(chainPEM)

	// Verify cert part
	if !bytes.Contains([]byte(certOnly), []byte("-----BEGIN CERTIFICATE-----")) {
		t.Error("cert part missing BEGIN marker")
	}

	// Verify chain part
	if !bytes.Contains([]byte(chain), []byte("-----BEGIN CERTIFICATE-----")) {
		t.Error("chain part missing BEGIN marker")
	}

	// Verify they're different
	if certOnly == chain {
		t.Error("cert and chain should be different")
	}
}

// TestSplitPEMChain_SingleCert tests PEM chain splitting with single certificate.
func TestSplitPEMChain_SingleCert(t *testing.T) {
	cert, _ := generateTestCertWithCN("example.com")
	block := &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}
	certPEM := string(pem.EncodeToMemory(block))

	certOnly, chain := splitPEMChain(certPEM)

	if certOnly != certPEM {
		t.Error("single cert should be returned as-is")
	}
	if chain != "" {
		t.Error("chain should be empty for single cert")
	}
}

// TestSplitPEMChain_InvalidPEM tests PEM chain splitting with invalid PEM.
func TestSplitPEMChain_InvalidPEM(t *testing.T) {
	invalidPEM := "not a valid pem"

	certOnly, chain := splitPEMChain(invalidPEM)

	if certOnly != invalidPEM {
		t.Error("invalid PEM should be returned as-is in cert part")
	}
	if chain != "" {
		t.Error("chain should be empty for invalid PEM")
	}
}

// TestParsePEMFile tests parsing a PEM file with certificates.
func TestParsePEMFile(t *testing.T) {
	// Create a temporary file with a PEM certificate
	tmpdir := t.TempDir()
	certPath := filepath.Join(tmpdir, "cert.pem")

	cert, _ := generateTestCert()
	block := &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}
	certPEM := pem.EncodeToMemory(block)

	if err := os.WriteFile(certPath, certPEM, 0644); err != nil {
		t.Fatalf("failed to write test cert: %v", err)
	}

	cfg := &AgentConfig{
		ServerURL: "http://localhost:8443",
		APIKey:    "test-key",
		AgentID:   "a-test",
		Hostname:  "test-host",
	}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	agent, _ := NewAgent(cfg, logger)

	// Parse the file
	entries := agent.parsePEMFile(certPath)

	if len(entries) != 1 {
		t.Errorf("expected 1 certificate, got %d", len(entries))
		return
	}

	entry := entries[0]
	if entry.CommonName != "test.example.com" {
		t.Errorf("expected CN 'test.example.com', got '%s'", entry.CommonName)
	}
	if entry.SourceFormat != "PEM" {
		t.Errorf("expected format 'PEM', got '%s'", entry.SourceFormat)
	}
	if entry.SourcePath != certPath {
		t.Errorf("expected path '%s', got '%s'", certPath, entry.SourcePath)
	}

	// Verify fingerprint is non-empty and correct length (SHA256 hex = 64 chars)
	if len(entry.FingerprintSHA256) != 64 {
		t.Errorf("expected 64-char fingerprint, got %d", len(entry.FingerprintSHA256))
	}
}

// TestParsePEMFile_MultipleCerts tests parsing a PEM file with multiple certificates.
func TestParsePEMFile_MultipleCerts(t *testing.T) {
	tmpdir := t.TempDir()
	certPath := filepath.Join(tmpdir, "chain.pem")

	cert1, _ := generateTestCertWithCN("cert1.example.com")
	cert2, _ := generateTestCertWithCN("cert2.example.com")

	block1 := &pem.Block{Type: "CERTIFICATE", Bytes: cert1.Raw}
	block2 := &pem.Block{Type: "CERTIFICATE", Bytes: cert2.Raw}

	certPEM := append(pem.EncodeToMemory(block1), pem.EncodeToMemory(block2)...)

	if err := os.WriteFile(certPath, certPEM, 0644); err != nil {
		t.Fatalf("failed to write test cert: %v", err)
	}

	cfg := &AgentConfig{
		ServerURL: "http://localhost:8443",
		APIKey:    "test-key",
		AgentID:   "a-test",
		Hostname:  "test-host",
	}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	agent, _ := NewAgent(cfg, logger)

	entries := agent.parsePEMFile(certPath)

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

// TestParseDERFile tests parsing a DER-encoded certificate file.
func TestParseDERFile(t *testing.T) {
	tmpdir := t.TempDir()
	derPath := filepath.Join(tmpdir, "cert.der")

	cert, _ := generateTestCertWithCN("test.example.com")
	if err := os.WriteFile(derPath, cert.Raw, 0644); err != nil {
		t.Fatalf("failed to write test cert: %v", err)
	}

	cfg := &AgentConfig{
		ServerURL: "http://localhost:8443",
		APIKey:    "test-key",
		AgentID:   "a-test",
		Hostname:  "test-host",
	}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	agent, _ := NewAgent(cfg, logger)

	entry, err := agent.parseDERFile(derPath)
	if err != nil {
		t.Errorf("unexpected error: %v", err)
		return
	}

	if entry.CommonName != "test.example.com" {
		t.Errorf("expected CN 'test.example.com', got '%s'", entry.CommonName)
	}
	if entry.SourceFormat != "DER" {
		t.Errorf("expected format 'DER', got '%s'", entry.SourceFormat)
	}
	if len(entry.FingerprintSHA256) != 64 {
		t.Errorf("expected 64-char fingerprint, got %d", len(entry.FingerprintSHA256))
	}
}

// TestParseDERFile_Invalid tests parsing an invalid DER file.
func TestParseDERFile_Invalid(t *testing.T) {
	tmpdir := t.TempDir()
	derPath := filepath.Join(tmpdir, "invalid.der")

	if err := os.WriteFile(derPath, []byte("not a valid der file"), 0644); err != nil {
		t.Fatalf("failed to write test file: %v", err)
	}

	cfg := &AgentConfig{
		ServerURL: "http://localhost:8443",
		APIKey:    "test-key",
		AgentID:   "a-test",
		Hostname:  "test-host",
	}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	agent, _ := NewAgent(cfg, logger)

	_, err := agent.parseDERFile(derPath)
	if err == nil {
		t.Error("expected error for invalid DER file")
	}
}

// TestScanDirectory tests scanning a directory for certificate files.
func TestScanDirectory(t *testing.T) {
	tmpdir := t.TempDir()

	// Create subdirectory
	subdir := filepath.Join(tmpdir, "subdir")
	if err := os.MkdirAll(subdir, 0755); err != nil {
		t.Fatalf("failed to create subdir: %v", err)
	}

	// Create certificates with various extensions
	cert1, _ := generateTestCertWithCN("cert1.example.com")
	cert2, _ := generateTestCertWithCN("cert2.example.com")

	// Write cert1.pem
	block1 := &pem.Block{Type: "CERTIFICATE", Bytes: cert1.Raw}
	if err := os.WriteFile(filepath.Join(tmpdir, "cert1.pem"), pem.EncodeToMemory(block1), 0644); err != nil {
		t.Fatalf("failed to write cert1: %v", err)
	}

	// Write cert2.crt in subdir
	block2 := &pem.Block{Type: "CERTIFICATE", Bytes: cert2.Raw}
	if err := os.WriteFile(filepath.Join(subdir, "cert2.crt"), pem.EncodeToMemory(block2), 0644); err != nil {
		t.Fatalf("failed to write cert2: %v", err)
	}

	cfg := &AgentConfig{
		ServerURL:     "http://localhost:8443",
		APIKey:        "test-key",
		AgentID:       "a-test",
		Hostname:      "test-host",
		DiscoveryDirs: []string{tmpdir},
	}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	agent, _ := NewAgent(cfg, logger)

	// Simulate directory walk manually (as runDiscoveryScan does)
	var certs []discoveredCertEntry
	filepath.Walk(tmpdir, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return nil
		}
		if info.IsDir() {
			return nil
		}

		ext := filepath.Ext(path)
		switch ext {
		case ".pem", ".crt":
			found := agent.parsePEMFile(path)
			certs = append(certs, found...)
		}
		return nil
	})

	if len(certs) != 2 {
		t.Errorf("expected 2 certificates from directory scan, got %d", len(certs))
	}
}

// TestCreateTargetConnector_NGINX tests connector creation for NGINX target.
func TestCreateTargetConnector_NGINX(t *testing.T) {
	cfg := &AgentConfig{
		ServerURL: "http://localhost:8443",
		APIKey:    "test-key",
		AgentID:   "a-test",
		Hostname:  "test-host",
	}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	agent, _ := NewAgent(cfg, logger)

	configJSON := json.RawMessage(`{"cert_path":"/etc/nginx/cert.pem"}`)
	connector, err := agent.createTargetConnector("NGINX", configJSON)

	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	if connector == nil {
		t.Error("expected connector to be non-nil")
	}
}

// TestCreateTargetConnector_Unsupported tests connector creation for unsupported type.
func TestCreateTargetConnector_Unsupported(t *testing.T) {
	cfg := &AgentConfig{
		ServerURL: "http://localhost:8443",
		APIKey:    "test-key",
		AgentID:   "a-test",
		Hostname:  "test-host",
	}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	agent, _ := NewAgent(cfg, logger)

	_, err := agent.createTargetConnector("UnsupportedType", nil)

	if err == nil {
		t.Error("expected error for unsupported target type")
	}
}

// TestFetchCertificate_Success tests fetching a certificate from the control plane.
func TestFetchCertificate_Success(t *testing.T) {
	cert, _ := generateTestCertWithCN("test.example.com")
	block := &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}
	expectedCertPEM := string(pem.EncodeToMemory(block))

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/api/v1/agents/a-test/certificates/mc-001" {
			t.Errorf("unexpected path: %s", r.URL.Path)
		}

		w.WriteHeader(http.StatusOK)
		json.NewEncoder(w).Encode(map[string]string{
			"certificate_pem": expectedCertPEM,
		})
	}))
	defer server.Close()

	cfg := &AgentConfig{
		ServerURL: server.URL,
		APIKey:    "test-key",
		AgentID:   "a-test",
		Hostname:  "test-host",
	}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	agent, _ := NewAgent(cfg, logger)

	certPEM, err := agent.fetchCertificate(context.Background(), "mc-001")
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	if certPEM != expectedCertPEM {
		t.Error("certificate PEM mismatch")
	}
}

// TestFetchCertificate_NotFound tests fetching a non-existent certificate.
func TestFetchCertificate_NotFound(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusNotFound)
		w.Write([]byte("not found"))
	}))
	defer server.Close()

	cfg := &AgentConfig{
		ServerURL: server.URL,
		APIKey:    "test-key",
		AgentID:   "a-test",
		Hostname:  "test-host",
	}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	agent, _ := NewAgent(cfg, logger)

	_, err := agent.fetchCertificate(context.Background(), "mc-nonexistent")
	if err == nil {
		t.Error("expected error for non-existent certificate")
	}
}

// TestReportJobStatus_Success tests reporting job status to the control plane.
func TestReportJobStatus_Success(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/api/v1/agents/a-test/jobs/j-001/status" {
			t.Errorf("unexpected path: %s", r.URL.Path)
		}
		if r.Method != http.MethodPost {
			t.Errorf("unexpected method: %s", r.Method)
		}

		var payload map[string]string
		json.NewDecoder(r.Body).Decode(&payload)

		if payload["status"] != "Completed" {
			t.Errorf("expected status 'Completed', got '%s'", payload["status"])
		}

		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()

	cfg := &AgentConfig{
		ServerURL: server.URL,
		APIKey:    "test-key",
		AgentID:   "a-test",
		Hostname:  "test-host",
	}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	agent, _ := NewAgent(cfg, logger)

	err := agent.reportJobStatus(context.Background(), "j-001", "Completed", "")
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
}

// TestReportJobStatus_WithError tests reporting job status with error message.
func TestReportJobStatus_WithError(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		var payload map[string]string
		json.NewDecoder(r.Body).Decode(&payload)

		if payload["status"] != "Failed" {
			t.Errorf("expected status 'Failed', got '%s'", payload["status"])
		}
		if payload["error"] != "deployment failed" {
			t.Errorf("expected error 'deployment failed', got '%s'", payload["error"])
		}

		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()

	cfg := &AgentConfig{
		ServerURL: server.URL,
		APIKey:    "test-key",
		AgentID:   "a-test",
		Hostname:  "test-host",
	}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	agent, _ := NewAgent(cfg, logger)

	err := agent.reportJobStatus(context.Background(), "j-001", "Failed", "deployment failed")
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
}

// TestMakeRequest_Success tests making an authenticated HTTP request.
func TestMakeRequest_Success(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Verify auth header
		auth := r.Header.Get("Authorization")
		if auth != "Bearer test-key" {
			t.Errorf("unexpected auth: %s", auth)
		}

		// Verify content-type
		ct := r.Header.Get("Content-Type")
		if ct != "application/json" {
			t.Errorf("unexpected content-type: %s", ct)
		}

		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()

	cfg := &AgentConfig{
		ServerURL: server.URL,
		APIKey:    "test-key",
		AgentID:   "a-test",
		Hostname:  "test-host",
	}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	agent, _ := NewAgent(cfg, logger)

	resp, err := agent.makeRequest(context.Background(), http.MethodPost, "/test", map[string]string{"key": "value"})
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		t.Errorf("unexpected status: %d", resp.StatusCode)
	}
}

// TestMakeRequest_InvalidURL tests making a request with invalid URL.
func TestMakeRequest_InvalidURL(t *testing.T) {
	cfg := &AgentConfig{
		ServerURL: "http://invalid-host-that-does-not-exist.local:9999",
		APIKey:    "test-key",
		AgentID:   "a-test",
		Hostname:  "test-host",
	}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	agent, _ := NewAgent(cfg, logger)

	_, err := agent.makeRequest(context.Background(), http.MethodGet, "/test", nil)
	if err == nil {
		t.Error("expected error for unreachable host")
	}
}

// TestCertKeyInfo tests extraction of key algorithm and size from certificates.
func TestCertKeyInfo(t *testing.T) {
	tests := []struct {
		name         string
		genKey       func() interface{}
		expectedAlg  string
		minBitSize   int
	}{
		{
			name: "ECDSA P-256",
			genKey: func() interface{} {
				key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
				return key.Public()
			},
			expectedAlg: "ECDSA",
			minBitSize:  256,
		},
		{
			name: "RSA 2048",
			genKey: func() interface{} {
				key, _ := rsa.GenerateKey(rand.Reader, 2048)
				return key.Public()
			},
			expectedAlg: "RSA",
			minBitSize:  2048,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			pubKey := tt.genKey()

			// Create certificate with this key
			template := &x509.Certificate{
				SerialNumber: big.NewInt(1),
				Subject: pkix.Name{
					CommonName: "test.com",
				},
				NotBefore:             time.Now(),
				NotAfter:              time.Now().Add(24 * time.Hour),
				KeyUsage:              x509.KeyUsageDigitalSignature,
				BasicConstraintsValid: true,
			}

			var privKey interface{}
			if ecdsaPub, ok := pubKey.(*ecdsa.PublicKey); ok {
				key, _ := ecdsa.GenerateKey(ecdsaPub.Curve, rand.Reader)
				privKey = key
			} else if rsaPub, ok := pubKey.(*rsa.PublicKey); ok {
				key, _ := rsa.GenerateKey(rand.Reader, rsaPub.N.BitLen())
				privKey = key
			}

			certDER, _ := x509.CreateCertificate(rand.Reader, template, template, pubKey, privKey)
			cert, _ := x509.ParseCertificate(certDER)

			alg, bitSize := certKeyInfo(cert)
			if alg != tt.expectedAlg {
				t.Errorf("expected algorithm %s, got %s", tt.expectedAlg, alg)
			}
			if bitSize < tt.minBitSize {
				t.Errorf("expected bitsize >= %d, got %d", tt.minBitSize, bitSize)
			}
		})
	}
}

// TestNewAgent tests agent initialization.
func TestNewAgent(t *testing.T) {
	cfg := &AgentConfig{
		ServerURL: "http://localhost:8443",
		APIKey:    "test-key",
		AgentID:   "a-test",
		Hostname:  "test-host",
	}

	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	agent, _ := NewAgent(cfg, logger)

	if agent.config != cfg {
		t.Error("config not set correctly")
	}
	if agent.heartbeatInterval != 60*time.Second {
		t.Errorf("expected heartbeat interval 60s, got %v", agent.heartbeatInterval)
	}
	if agent.pollInterval != 30*time.Second {
		t.Errorf("expected poll interval 30s, got %v", agent.pollInterval)
	}
	if agent.client == nil {
		t.Error("HTTP client not initialized")
	}
}

// TestNewAgent_WithLogger tests agent initialization with logger.
func TestNewAgent_WithLogger(t *testing.T) {
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	cfg := &AgentConfig{
		ServerURL: "http://localhost:8443",
		APIKey:    "test-key",
		AgentID:   "a-test",
		Hostname:  "test-host",
	}

	agent, _ := NewAgent(cfg, logger)

	if agent.logger != logger {
		t.Error("logger not set correctly")
	}
}

// Helper to create test certificates with specific CN
func generateTestCertWithCN(commonName string) (*x509.Certificate, error) {
	key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		return nil, err
	}

	template := &x509.Certificate{
		SerialNumber: big.NewInt(1),
		Subject: pkix.Name{
			CommonName: commonName,
		},
		NotBefore:             time.Now(),
		NotAfter:              time.Now().Add(24 * time.Hour),
		KeyUsage:              x509.KeyUsageDigitalSignature,
		BasicConstraintsValid: true,
		DNSNames:              []string{commonName},
	}

	certDER, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
	if err != nil {
		return nil, err
	}

	return x509.ParseCertificate(certDER)
}

// Helper to create string pointer
func strPtr(s string) *string {
	return &s
}

// TestCreateTargetConnector_AllSupportedTypes tests connector creation for all 14 supported target types.
func TestCreateTargetConnector_AllSupportedTypes(t *testing.T) {
	tmpDir := t.TempDir()

	tests := []struct {
		name     string
		typeName string
		config   interface{}
	}{
		{
			name:     "NGINX",
			typeName: "NGINX",
			config: map[string]string{
				"cert_path": filepath.Join(tmpDir, "cert.pem"),
				"key_path":  filepath.Join(tmpDir, "key.pem"),
			},
		},
		{
			name:     "Apache",
			typeName: "Apache",
			config: map[string]string{
				"cert_path": filepath.Join(tmpDir, "cert.pem"),
				"key_path":  filepath.Join(tmpDir, "key.pem"),
			},
		},
		{
			name:     "HAProxy",
			typeName: "HAProxy",
			config: map[string]string{
				"cert_path": filepath.Join(tmpDir, "cert.pem"),
			},
		},
		{
			name:     "F5",
			typeName: "F5",
			config: map[string]string{
				"host": "192.0.2.1",
			},
		},
		{
			name:     "IIS",
			typeName: "IIS",
			config: map[string]string{
				"cert_store": "My",
			},
		},
		{
			name:     "Traefik",
			typeName: "Traefik",
			config: map[string]string{
				"cert_dir": tmpDir,
			},
		},
		{
			name:     "Caddy",
			typeName: "Caddy",
			config: map[string]string{
				"mode": "file",
			},
		},
		{
			name:     "Envoy",
			typeName: "Envoy",
			config: map[string]string{
				"cert_dir": tmpDir,
			},
		},
		{
			name:     "Postfix",
			typeName: "Postfix",
			config: map[string]string{
				"cert_path": filepath.Join(tmpDir, "cert.pem"),
				"key_path":  filepath.Join(tmpDir, "key.pem"),
			},
		},
		{
			name:     "Dovecot",
			typeName: "Dovecot",
			config: map[string]string{
				"cert_path": filepath.Join(tmpDir, "cert.pem"),
				"key_path":  filepath.Join(tmpDir, "key.pem"),
			},
		},
		{
			name:     "SSH",
			typeName: "SSH",
			config: map[string]string{
				"host":      "192.0.2.1",
				"user":      "root",
				"cert_path": "/etc/ssl/cert.pem",
				"key_path":  "/etc/ssl/key.pem",
			},
		},
		{
			name:     "WinCertStore",
			typeName: "WinCertStore",
			config: map[string]string{
				"cert_store": "My",
			},
		},
		{
			name:     "JavaKeystore",
			typeName: "JavaKeystore",
			config: map[string]string{
				"keystore_path": filepath.Join(tmpDir, "keystore.jks"),
			},
		},
		{
			name:     "KubernetesSecrets",
			typeName: "KubernetesSecrets",
			config: map[string]string{
				"namespace":   "default",
				"secret_name": "tls-secret",
			},
		},
	}

	cfg := &AgentConfig{
		ServerURL: "http://localhost:8443",
		APIKey:    "test-key",
		AgentID:   "a-test",
		Hostname:  "test-host",
	}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	agent, _ := NewAgent(cfg, logger)

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			configJSON, err := json.Marshal(tt.config)
			if err != nil {
				t.Fatalf("failed to marshal config: %v", err)
			}

			connector, err := agent.createTargetConnector(tt.typeName, configJSON)

			// Some connectors (like WinCertStore, IIS) may error on non-Windows platforms
			// or with insufficient validation. We accept either a valid connector or an error
			// for now — the real unit tests in internal/connector/target/* cover validation
			if connector == nil && err != nil {
				// This is acceptable if the connector validates required fields
				t.Logf("connector creation returned error (may be validation): %v", err)
				return
			}

			if connector == nil {
				t.Errorf("expected connector to be non-nil for type %s", tt.typeName)
			}
		})
	}
}

// TestCreateTargetConnector_InvalidJSON tests connector creation with invalid JSON for each type.
func TestCreateTargetConnector_InvalidJSON(t *testing.T) {
	tests := []string{
		"NGINX",
		"Apache",
		"HAProxy",
		"F5",
		"IIS",
		"Traefik",
		"Caddy",
		"Envoy",
		"Postfix",
		"Dovecot",
		"SSH",
		"WinCertStore",
		"JavaKeystore",
		"KubernetesSecrets",
	}

	cfg := &AgentConfig{
		ServerURL: "http://localhost:8443",
		APIKey:    "test-key",
		AgentID:   "a-test",
		Hostname:  "test-host",
	}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	agent, _ := NewAgent(cfg, logger)

	invalidJSON := json.RawMessage("{invalid json}")

	for _, typeName := range tests {
		t.Run(typeName, func(t *testing.T) {
			_, err := agent.createTargetConnector(typeName, invalidJSON)

			if err == nil {
				t.Errorf("expected error for invalid JSON with type %s", typeName)
			}
		})
	}
}

// TestCreateTargetConnector_UnknownType tests connector creation with unknown target type.
func TestCreateTargetConnector_UnknownType(t *testing.T) {
	cfg := &AgentConfig{
		ServerURL: "http://localhost:8443",
		APIKey:    "test-key",
		AgentID:   "a-test",
		Hostname:  "test-host",
	}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	agent, _ := NewAgent(cfg, logger)

	_, err := agent.createTargetConnector("MagicBox", nil)

	if err == nil {
		t.Error("expected error for unsupported target type")
	}
	if !strings.Contains(err.Error(), "unsupported target type") {
		t.Errorf("expected 'unsupported target type' error, got: %v", err)
	}
}

// TestCreateTargetConnector_EmptyConfig tests connector creation with empty config JSON.
func TestCreateTargetConnector_EmptyConfig(t *testing.T) {
	tests := []string{
		"NGINX",
		"Apache",
		"HAProxy",
		"Traefik",
		"Caddy",
		"Envoy",
	}

	cfg := &AgentConfig{
		ServerURL: "http://localhost:8443",
		APIKey:    "test-key",
		AgentID:   "a-test",
		Hostname:  "test-host",
	}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	agent, _ := NewAgent(cfg, logger)

	for _, typeName := range tests {
		t.Run(typeName, func(t *testing.T) {
			// Empty config should be handled gracefully (defaults applied)
			connector, err := agent.createTargetConnector(typeName, nil)

			// Should not error on nil/empty config (defaults are applied)
			if err != nil {
				// Validation errors are acceptable, but parsing errors are not
				if !strings.Contains(err.Error(), "invalid") && !strings.Contains(err.Error(), "missing") {
					t.Logf("connector creation with empty config returned: %v", err)
				}
				return
			}

			if connector == nil {
				t.Errorf("expected non-nil connector for type %s with empty config", typeName)
			}
		})
	}
}

// TestRunDiscoveryScan_ValidCerts tests discovery scanning with valid certificates.
func TestRunDiscoveryScan_ValidCerts(t *testing.T) {
	tmpDir := t.TempDir()

	// Create a valid PEM certificate file
	cert, _ := generateTestCertWithCN("example.com")
	block := &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}
	certPEM := pem.EncodeToMemory(block)

	certPath := filepath.Join(tmpDir, "cert.pem")
	if err := os.WriteFile(certPath, certPEM, 0644); err != nil {
		t.Fatalf("failed to write certificate: %v", err)
	}

	// Mock server to accept discovery report
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/api/v1/agents/a-test/discoveries" {
			t.Errorf("unexpected path: %s", r.URL.Path)
			w.WriteHeader(http.StatusNotFound)
			return
		}
		if r.Method != http.MethodPost {
			t.Errorf("unexpected method: %s", r.Method)
			w.WriteHeader(http.StatusMethodNotAllowed)
			return
		}

		// Verify request body
		var payload map[string]interface{}
		if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
			t.Logf("failed to decode discovery report: %v", err)
			w.WriteHeader(http.StatusBadRequest)
			return
		}

		// Verify report contains certificates
		certs, ok := payload["certificates"].([]interface{})
		if !ok || len(certs) == 0 {
			t.Logf("expected certificates in report")
		}

		w.WriteHeader(http.StatusAccepted)
	}))
	defer server.Close()

	cfg := &AgentConfig{
		ServerURL:     server.URL,
		APIKey:        "test-key",
		AgentID:       "a-test",
		Hostname:      "test-host",
		DiscoveryDirs: []string{tmpDir},
	}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	agent, _ := NewAgent(cfg, logger)

	// Run discovery scan
	agent.runDiscoveryScan(context.Background())

	// If we got here without panic/error, the test passes
}

// TestRunDiscoveryScan_NoCertificates tests discovery scanning with empty directory.
func TestRunDiscoveryScan_NoCertificates(t *testing.T) {
	tmpDir := t.TempDir()

	// Create an empty directory
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Should not receive a request if no certs found and no errors
		t.Logf("discovery report received: %s", r.URL.Path)
		w.WriteHeader(http.StatusAccepted)
	}))
	defer server.Close()

	cfg := &AgentConfig{
		ServerURL:     server.URL,
		APIKey:        "test-key",
		AgentID:       "a-test",
		Hostname:      "test-host",
		DiscoveryDirs: []string{tmpDir},
	}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	agent, _ := NewAgent(cfg, logger)

	// Run discovery scan - should complete without error even with empty directory
	agent.runDiscoveryScan(context.Background())
}

// TestRunDiscoveryScan_MultipleCerts tests discovery scanning with multiple certificate files.
func TestRunDiscoveryScan_MultipleCerts(t *testing.T) {
	tmpDir := t.TempDir()

	// Create multiple certificate files
	cert1, _ := generateTestCertWithCN("cert1.example.com")
	cert2, _ := generateTestCertWithCN("cert2.example.com")

	block1 := &pem.Block{Type: "CERTIFICATE", Bytes: cert1.Raw}
	block2 := &pem.Block{Type: "CERTIFICATE", Bytes: cert2.Raw}

	certPath1 := filepath.Join(tmpDir, "cert1.pem")
	certPath2 := filepath.Join(tmpDir, "cert2.crt")

	if err := os.WriteFile(certPath1, pem.EncodeToMemory(block1), 0644); err != nil {
		t.Fatalf("failed to write cert1: %v", err)
	}
	if err := os.WriteFile(certPath2, pem.EncodeToMemory(block2), 0644); err != nil {
		t.Fatalf("failed to write cert2: %v", err)
	}

	certCount := 0
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/api/v1/agents/a-test/discoveries" {
			w.WriteHeader(http.StatusNotFound)
			return
		}

		var payload map[string]interface{}
		if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
			w.WriteHeader(http.StatusBadRequest)
			return
		}

		// Count certificates in report
		if certs, ok := payload["certificates"].([]interface{}); ok {
			certCount = len(certs)
		}

		w.WriteHeader(http.StatusAccepted)
	}))
	defer server.Close()

	cfg := &AgentConfig{
		ServerURL:     server.URL,
		APIKey:        "test-key",
		AgentID:       "a-test",
		Hostname:      "test-host",
		DiscoveryDirs: []string{tmpDir},
	}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	agent, _ := NewAgent(cfg, logger)

	// Run discovery scan
	agent.runDiscoveryScan(context.Background())

	if certCount != 2 {
		t.Logf("expected 2 certificates in discovery report, got %d", certCount)
	}
}

// TestRunDiscoveryScan_DERCertificate tests discovery scanning with DER-encoded certificate.
func TestRunDiscoveryScan_DERCertificate(t *testing.T) {
	tmpDir := t.TempDir()

	// Create a DER-encoded certificate file
	cert, _ := generateTestCertWithCN("der.example.com")
	derPath := filepath.Join(tmpDir, "cert.der")

	if err := os.WriteFile(derPath, cert.Raw, 0644); err != nil {
		t.Fatalf("failed to write DER certificate: %v", err)
	}

	certCount := 0
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/api/v1/agents/a-test/discoveries" {
			w.WriteHeader(http.StatusNotFound)
			return
		}

		var payload map[string]interface{}
		if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
			w.WriteHeader(http.StatusBadRequest)
			return
		}

		if certs, ok := payload["certificates"].([]interface{}); ok {
			certCount = len(certs)
		}

		w.WriteHeader(http.StatusAccepted)
	}))
	defer server.Close()

	cfg := &AgentConfig{
		ServerURL:     server.URL,
		APIKey:        "test-key",
		AgentID:       "a-test",
		Hostname:      "test-host",
		DiscoveryDirs: []string{tmpDir},
	}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	agent, _ := NewAgent(cfg, logger)

	// Run discovery scan
	agent.runDiscoveryScan(context.Background())

	if certCount != 1 {
		t.Logf("expected 1 DER certificate in discovery report, got %d", certCount)
	}
}

// TestRunDiscoveryScan_Subdirectories tests discovery scanning with subdirectories.
func TestRunDiscoveryScan_Subdirectories(t *testing.T) {
	tmpDir := t.TempDir()

	// Create subdirectory
	subDir := filepath.Join(tmpDir, "subdir")
	if err := os.MkdirAll(subDir, 0755); err != nil {
		t.Fatalf("failed to create subdir: %v", err)
	}

	// Create certificate in subdirectory
	cert, _ := generateTestCertWithCN("subdir.example.com")
	block := &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}
	certPath := filepath.Join(subDir, "cert.pem")

	if err := os.WriteFile(certPath, pem.EncodeToMemory(block), 0644); err != nil {
		t.Fatalf("failed to write certificate: %v", err)
	}

	certCount := 0
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path != "/api/v1/agents/a-test/discoveries" {
			w.WriteHeader(http.StatusNotFound)
			return
		}

		var payload map[string]interface{}
		if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
			w.WriteHeader(http.StatusBadRequest)
			return
		}

		if certs, ok := payload["certificates"].([]interface{}); ok {
			certCount = len(certs)
		}

		w.WriteHeader(http.StatusAccepted)
	}))
	defer server.Close()

	cfg := &AgentConfig{
		ServerURL:     server.URL,
		APIKey:        "test-key",
		AgentID:       "a-test",
		Hostname:      "test-host",
		DiscoveryDirs: []string{tmpDir},
	}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	agent, _ := NewAgent(cfg, logger)

	// Run discovery scan - should recursively find certs in subdirs
	agent.runDiscoveryScan(context.Background())

	if certCount != 1 {
		t.Logf("expected 1 certificate in subdirectory, got %d", certCount)
	}
}

// TestRunDiscoveryScan_ServerError tests discovery scanning when server returns error.
func TestRunDiscoveryScan_ServerError(t *testing.T) {
	tmpDir := t.TempDir()

	// Create a certificate file
	cert, _ := generateTestCertWithCN("example.com")
	block := &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}
	certPath := filepath.Join(tmpDir, "cert.pem")

	if err := os.WriteFile(certPath, pem.EncodeToMemory(block), 0644); err != nil {
		t.Fatalf("failed to write certificate: %v", err)
	}

	// Mock server returns error
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusInternalServerError)
		w.Write([]byte("server error"))
	}))
	defer server.Close()

	cfg := &AgentConfig{
		ServerURL:     server.URL,
		APIKey:        "test-key",
		AgentID:       "a-test",
		Hostname:      "test-host",
		DiscoveryDirs: []string{tmpDir},
	}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	agent, _ := NewAgent(cfg, logger)

	// Should handle server error gracefully without panicking
	agent.runDiscoveryScan(context.Background())
}

// TestDiscoveredCertEntry_ValidFields tests that discovered certificate entries have valid fields.
func TestDiscoveredCertEntry_ValidFields(t *testing.T) {
	tmpDir := t.TempDir()

	// Create certificate with specific details
	cert, _ := generateTestCertWithCN("test.example.com")
	block := &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}
	certPEM := pem.EncodeToMemory(block)

	certPath := filepath.Join(tmpDir, "cert.pem")
	if err := os.WriteFile(certPath, certPEM, 0644); err != nil {
		t.Fatalf("failed to write certificate: %v", err)
	}

	cfg := &AgentConfig{
		ServerURL: "http://localhost:8443",
		APIKey:    "test-key",
		AgentID:   "a-test",
		Hostname:  "test-host",
	}
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	agent, _ := NewAgent(cfg, logger)

	entries := agent.parsePEMFile(certPath)

	if len(entries) != 1 {
		t.Fatalf("expected 1 entry, got %d", len(entries))
	}

	entry := entries[0]

	// Verify all required fields are populated
	if entry.CommonName == "" {
		t.Error("CommonName should not be empty")
	}
	if entry.FingerprintSHA256 == "" {
		t.Error("FingerprintSHA256 should not be empty")
	}
	if len(entry.FingerprintSHA256) != 64 {
		t.Errorf("FingerprintSHA256 should be 64 hex chars, got %d", len(entry.FingerprintSHA256))
	}
	if entry.SerialNumber == "" {
		t.Error("SerialNumber should not be empty")
	}
	if entry.IssuerDN == "" {
		t.Error("IssuerDN should not be empty")
	}
	if entry.SubjectDN == "" {
		t.Error("SubjectDN should not be empty")
	}
	if entry.NotBefore == "" {
		t.Error("NotBefore should not be empty")
	}
	if entry.NotAfter == "" {
		t.Error("NotAfter should not be empty")
	}
	if entry.KeyAlgorithm == "" {
		t.Error("KeyAlgorithm should not be empty")
	}
	if entry.KeySize == 0 {
		t.Error("KeySize should not be zero")
	}
	if entry.SourcePath == "" {
		t.Error("SourcePath should not be empty")
	}
	if entry.SourceFormat != "PEM" {
		t.Errorf("SourceFormat should be 'PEM', got '%s'", entry.SourceFormat)
	}
	if entry.PEMData == "" {
		t.Error("PEMData should not be empty")
	}
}

// ---------------------------------------------------------------------------
// HTTPS-Everywhere milestone (v2.2, §3.2 / §7) — Phase 5 client-side tests.
//
// These tests pin the agent's pre-flight HTTPS-scheme guard and the TLS
// configuration surface (CA bundle loading + TLS 1.3 round-trip) so that
// regressions surface at unit-test time, not at the first heartbeat of a
// production rollout. Matches the same contract asserted by the sibling
// binaries cmd/cli/main_test.go and cmd/mcp-server/main_test.go — the three
// must stay in lock-step because all three are HTTPS-only clients of the
// same control plane.
// ---------------------------------------------------------------------------

// TestValidateHTTPSScheme pins the pre-flight URL-scheme guard that the
// HTTPS-Everywhere milestone requires on the agent binary startup path. The
// agent's diagnostic is distinct from the CLI/MCP variants because it names
// CERTCTL_SERVER_URL (the only input channel — no --server flag on the
// agent). Every case here mirrors the dispatch arms in cmd/agent/main.go:
// validateHTTPSScheme; drifting the error-message substrings is what this
// test is here to catch.
func TestValidateHTTPSScheme(t *testing.T) {
	tests := []struct {
		name       string
		serverURL  string
		wantErr    bool
		wantErrSub string
	}{
		{
			name:      "https URL passes",
			serverURL: "https://certctl-server:8443",
			wantErr:   false,
		},
		{
			name:      "https URL with path passes",
			serverURL: "https://certctl.example.com/api/v1",
			wantErr:   false,
		},
		{
			name:      "uppercase HTTPS scheme passes (url.Parse lowercases)",
			serverURL: "HTTPS://certctl-server:8443",
			wantErr:   false,
		},
		{
			name:       "empty URL rejected names CERTCTL_SERVER_URL",
			serverURL:  "",
			wantErr:    true,
			wantErrSub: "CERTCTL_SERVER_URL is empty",
		},
		{
			name:       "plaintext http rejected",
			serverURL:  "http://certctl-server:8443",
			wantErr:    true,
			wantErrSub: "plaintext http://",
		},
		{
			name:       "bare host missing scheme falls through to unsupported",
			serverURL:  "localhost:8443",
			wantErr:    true,
			// url.Parse treats "localhost:8443" as scheme=localhost,
			// opaque=8443 — exercises the default arm (unsupported scheme)
			// rather than the empty-scheme arm. Both are fail-closed, which
			// is what we care about.
			wantErrSub: "unsupported scheme",
		},
		{
			name:       "path-only URL rejected",
			serverURL:  "//certctl-server:8443",
			wantErr:    true,
			wantErrSub: "missing a scheme",
		},
		{
			name:       "unsupported scheme rejected",
			serverURL:  "ftp://certctl-server:8443",
			wantErr:    true,
			wantErrSub: "unsupported scheme",
		},
		{
			name:       "ws scheme rejected",
			serverURL:  "ws://certctl-server:8443",
			wantErr:    true,
			wantErrSub: "unsupported scheme",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := validateHTTPSScheme(tt.serverURL)
			if (err != nil) != tt.wantErr {
				t.Fatalf("validateHTTPSScheme(%q) err=%v wantErr=%v", tt.serverURL, err, tt.wantErr)
			}
			if tt.wantErr && tt.wantErrSub != "" && !strings.Contains(err.Error(), tt.wantErrSub) {
				t.Errorf("validateHTTPSScheme(%q) err=%q must contain %q so operators see the right diagnostic",
					tt.serverURL, err.Error(), tt.wantErrSub)
			}
		})
	}
}

// writeTestCABundle PEM-encodes a cert's DER bytes and writes the result to a
// tmp file inside dir. Used by CA-bundle tests so each case owns a distinct
// file path (matters for the "missing file" case which must point at a path
// that provably does not exist). Returns the path.
func writeTestCABundle(t *testing.T, dir string, certDER []byte, filename string) string {
	t.Helper()
	pemBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
	path := filepath.Join(dir, filename)
	if err := os.WriteFile(path, pemBytes, 0644); err != nil {
		t.Fatalf("writing CA bundle %q: %v", path, err)
	}
	return path
}

// TestNewAgent_CABundle_Success confirms that a well-formed PEM bundle gets
// parsed into an x509.CertPool and wired onto the agent's HTTP client
// transport. This is the happy path the docs/tls.md "Private CA signed
// server cert" section depends on.
func TestNewAgent_CABundle_Success(t *testing.T) {
	cert, err := generateTestCertWithCN("test.certctl.local")
	if err != nil {
		t.Fatalf("generateTestCertWithCN: %v", err)
	}
	bundlePath := writeTestCABundle(t, t.TempDir(), cert.Raw, "ca-bundle.pem")

	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	agent, err := NewAgent(&AgentConfig{
		ServerURL:    "https://certctl-server:8443",
		APIKey:       "test-key",
		AgentID:      "a-test",
		Hostname:     "test-host",
		CABundlePath: bundlePath,
	}, logger)
	if err != nil {
		t.Fatalf("NewAgent with valid CA bundle err=%v want nil", err)
	}

	transport, ok := agent.client.Transport.(*http.Transport)
	if !ok {
		t.Fatalf("agent.client.Transport is %T; want *http.Transport", agent.client.Transport)
	}
	if transport.TLSClientConfig == nil {
		t.Fatal("TLSClientConfig is nil; HTTPS-everywhere milestone requires a non-nil TLS config")
	}
	if transport.TLSClientConfig.MinVersion != tls.VersionTLS13 {
		t.Errorf("MinVersion=%x want TLS 1.3 (%x) per §2.3 of the milestone spec",
			transport.TLSClientConfig.MinVersion, tls.VersionTLS13)
	}
	if transport.TLSClientConfig.RootCAs == nil {
		t.Error("RootCAs is nil; the configured CA bundle was silently dropped")
	}
}

// TestNewAgent_CABundle_MissingFile pins the fail-loud behavior when the
// operator points CERTCTL_SERVER_CA_BUNDLE_PATH at a path that does not
// exist. Falling back to system roots here would mask a misconfiguration as
// a much harder-to-debug TLS handshake failure downstream.
func TestNewAgent_CABundle_MissingFile(t *testing.T) {
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	missingPath := filepath.Join(t.TempDir(), "does-not-exist.pem")
	_, err := NewAgent(&AgentConfig{
		ServerURL:    "https://certctl-server:8443",
		APIKey:       "test-key",
		AgentID:      "a-test",
		Hostname:     "test-host",
		CABundlePath: missingPath,
	}, logger)
	if err == nil {
		t.Fatal("NewAgent err=nil for missing CA bundle path; must fail loud at startup")
	}
	if !strings.Contains(err.Error(), "reading CA bundle") {
		t.Errorf("err=%q must contain \"reading CA bundle\" so operators can trace the cause", err.Error())
	}
}

// TestNewAgent_CABundle_EmptyPEM covers the "file exists but contains no
// valid certs" case (garbage, wrong-format, stripped PEM). AppendCertsFromPEM
// returns false in this case; NewAgent must translate that into a fail-loud
// startup error rather than quietly carry on with an empty pool.
func TestNewAgent_CABundle_EmptyPEM(t *testing.T) {
	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	bundlePath := filepath.Join(t.TempDir(), "empty.pem")
	if err := os.WriteFile(bundlePath, []byte("not a pem-encoded certificate, just garbage\n"), 0644); err != nil {
		t.Fatalf("writing garbage bundle: %v", err)
	}
	_, err := NewAgent(&AgentConfig{
		ServerURL:    "https://certctl-server:8443",
		APIKey:       "test-key",
		AgentID:      "a-test",
		Hostname:     "test-host",
		CABundlePath: bundlePath,
	}, logger)
	if err == nil {
		t.Fatal("NewAgent err=nil for empty-PEM CA bundle; must fail loud at startup")
	}
	if !strings.Contains(err.Error(), "no valid PEM-encoded certificates") {
		t.Errorf("err=%q must contain \"no valid PEM-encoded certificates\" so operators see why the bundle was rejected", err.Error())
	}
}

// TestNewAgent_TLSRoundTrip is the end-to-end integration-style check: spin
// up an httptest.NewTLSServer (which presents a self-signed cert over TLS
// 1.3), feed that cert into the agent as a CA bundle, and confirm the agent
// successfully completes a heartbeat round-trip over HTTPS. This proves that
// (a) the CA pool is actually being consulted during verification and (b)
// the TLS 1.3 MinVersion doesn't break against httptest's default
// negotiation. Equivalent to the "TLS handshake succeeds against a
// self-signed control plane" integration gate, but runs in-process with no
// Docker dependency.
func TestNewAgent_TLSRoundTrip(t *testing.T) {
	var heartbeatHit int
	server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/api/v1/agents/a-tls-test/heartbeat" && r.Method == http.MethodPost {
			heartbeatHit++
			w.WriteHeader(http.StatusOK)
			return
		}
		w.WriteHeader(http.StatusNotFound)
	}))
	defer server.Close()

	// server.Certificate() returns the *x509.Certificate httptest presents;
	// PEM-encode its DER bytes so NewAgent's AppendCertsFromPEM can ingest it.
	bundlePath := writeTestCABundle(t, t.TempDir(), server.Certificate().Raw, "httptest-ca.pem")

	logger := slog.New(slog.NewTextHandler(io.Discard, nil))
	agent, err := NewAgent(&AgentConfig{
		ServerURL:    server.URL,
		APIKey:       "test-key",
		AgentID:      "a-tls-test",
		Hostname:     "tls-test-host",
		CABundlePath: bundlePath,
	}, logger)
	if err != nil {
		t.Fatalf("NewAgent with httptest CA bundle err=%v want nil", err)
	}

	agent.sendHeartbeat(context.Background())

	if heartbeatHit != 1 {
		t.Fatalf("heartbeat handler hit %d times; want 1 — the TLS round-trip must actually complete", heartbeatHit)
	}
}
