package azurekv_test

// Rank 5 of the 2026-05-03 Infisical deep-research deliverable
// (the project's deep-research deliverable, Part 5). Happy-path tests
// for the Azure Key Vault target connector. Mirrors the awsacm_test.go
// shape so cross-cloud regressions are bisectable side-by-side.

import (
	"context"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/json"
	"encoding/pem"
	"errors"
	"fmt"
	"io"
	"log/slog"
	"math/big"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/certctl-io/certctl/internal/connector/target"
	"github.com/certctl-io/certctl/internal/connector/target/azurekv"
)

// mockKeyVaultClient fakes the KeyVaultClient interface seam.
type mockKeyVaultClient struct {
	mu sync.Mutex

	importCalls       []*azurekv.ImportCertificateInput
	getCalls          []*azurekv.GetCertificateInput
	listVersionsCalls []*azurekv.ListVersionsInput

	importOutput *azurekv.ImportCertificateOutput
	importErr    error
	getOutput    *azurekv.GetCertificateOutput
	getErr       error
	listOutput   *azurekv.ListVersionsOutput
	listErr      error

	rollbackImportErr error // injected on the SECOND import call
}

func (m *mockKeyVaultClient) ImportCertificate(ctx context.Context, in *azurekv.ImportCertificateInput) (*azurekv.ImportCertificateOutput, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.importCalls = append(m.importCalls, in)
	if len(m.importCalls) > 1 && m.rollbackImportErr != nil {
		return nil, m.rollbackImportErr
	}
	if m.importErr != nil {
		return nil, m.importErr
	}
	if m.importOutput != nil {
		return m.importOutput, nil
	}
	return &azurekv.ImportCertificateOutput{
		VersionID: "01234567890abcdef01234567890abcd",
		KID:       "https://test-vault.vault.azure.net/certificates/" + in.CertificateName + "/01234567890abcdef01234567890abcd",
	}, nil
}

func (m *mockKeyVaultClient) GetCertificate(ctx context.Context, in *azurekv.GetCertificateInput) (*azurekv.GetCertificateOutput, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.getCalls = append(m.getCalls, in)
	if m.getErr != nil {
		return nil, m.getErr
	}
	if m.getOutput != nil {
		return m.getOutput, nil
	}
	return &azurekv.GetCertificateOutput{}, nil
}

func (m *mockKeyVaultClient) ListVersions(ctx context.Context, in *azurekv.ListVersionsInput) (*azurekv.ListVersionsOutput, error) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.listVersionsCalls = append(m.listVersionsCalls, in)
	if m.listErr != nil {
		return nil, m.listErr
	}
	if m.listOutput != nil {
		return m.listOutput, nil
	}
	return &azurekv.ListVersionsOutput{}, nil
}

func quietTestLogger() *slog.Logger {
	return slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{Level: slog.LevelError}))
}

// generateTestCert creates a self-signed ECDSA P-256 cert + key for tests.
// Mirrors the awsacm test helper but emits the cert+key as separate
// PEM strings (the connector handles the PFX wrapping internally).
// Returns (certPEM, keyPEM, derBytes, serial).
func generateTestCert(t *testing.T, cn string) (certPEM, keyPEM string, derBytes []byte, serial string) {
	t.Helper()
	priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		t.Fatalf("generate key: %v", err)
	}
	serialNum, _ := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
	tmpl := &x509.Certificate{
		SerialNumber:          serialNum,
		Subject:               pkix.Name{CommonName: cn},
		NotBefore:             time.Now().Add(-time.Hour),
		NotAfter:              time.Now().Add(24 * time.Hour),
		BasicConstraintsValid: true,
		KeyUsage:              x509.KeyUsageDigitalSignature,
		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
	}
	der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &priv.PublicKey, priv)
	if err != nil {
		t.Fatalf("create cert: %v", err)
	}
	derBytes = der
	certPEM = string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}))
	keyDER, _ := x509.MarshalECPrivateKey(priv)
	keyPEM = string(pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER}))

	hex := fmt.Sprintf("%x", serialNum)
	if len(hex)%2 == 1 {
		hex = "0" + hex
	}
	var b strings.Builder
	for i := 0; i < len(hex); i += 2 {
		if i > 0 {
			b.WriteByte(':')
		}
		b.WriteString(hex[i : i+2])
	}
	serial = b.String()
	return
}

func TestAzureKV_ValidateConfig_Success(t *testing.T) {
	ctx := context.Background()
	c := azurekv.NewWithClient(nil, &mockKeyVaultClient{}, quietTestLogger())
	cfg := azurekv.Config{
		VaultURL:        "https://test-vault.vault.azure.net",
		CertificateName: "demo-cert",
	}
	raw, _ := json.Marshal(cfg)
	if err := c.ValidateConfig(ctx, raw); err != nil {
		t.Fatalf("ValidateConfig: %v", err)
	}
}

func TestAzureKV_ValidateConfig_MissingVaultURL(t *testing.T) {
	ctx := context.Background()
	c := azurekv.NewWithClient(nil, &mockKeyVaultClient{}, quietTestLogger())
	raw, _ := json.Marshal(azurekv.Config{CertificateName: "x"})
	err := c.ValidateConfig(ctx, raw)
	if err == nil || !strings.Contains(err.Error(), "vault_url is required") {
		t.Errorf("expected vault_url-required error; got %v", err)
	}
}

func TestAzureKV_ValidateConfig_MalformedVaultURL(t *testing.T) {
	ctx := context.Background()
	c := azurekv.NewWithClient(nil, &mockKeyVaultClient{}, quietTestLogger())
	raw, _ := json.Marshal(azurekv.Config{
		VaultURL:        "http://not-https",
		CertificateName: "demo",
	})
	err := c.ValidateConfig(ctx, raw)
	if err == nil || !strings.Contains(err.Error(), "vault_url malformed") {
		t.Errorf("expected vault_url-malformed error; got %v", err)
	}
}

func TestAzureKV_ValidateConfig_MissingCertName(t *testing.T) {
	ctx := context.Background()
	c := azurekv.NewWithClient(nil, &mockKeyVaultClient{}, quietTestLogger())
	raw, _ := json.Marshal(azurekv.Config{VaultURL: "https://x.vault.azure.net"})
	err := c.ValidateConfig(ctx, raw)
	if err == nil || !strings.Contains(err.Error(), "certificate_name is required") {
		t.Errorf("expected cert-name-required error; got %v", err)
	}
}

func TestAzureKV_ValidateConfig_InvalidCredentialMode(t *testing.T) {
	ctx := context.Background()
	c := azurekv.NewWithClient(nil, &mockKeyVaultClient{}, quietTestLogger())
	raw, _ := json.Marshal(azurekv.Config{
		VaultURL:        "https://test-vault.vault.azure.net",
		CertificateName: "demo",
		CredentialMode:  "invalid-mode",
	})
	err := c.ValidateConfig(ctx, raw)
	if err == nil || !strings.Contains(err.Error(), "credential_mode invalid") {
		t.Errorf("expected credential_mode-invalid error; got %v", err)
	}
}

func TestAzureKV_ValidateConfig_RejectsReservedTags(t *testing.T) {
	ctx := context.Background()
	c := azurekv.NewWithClient(nil, &mockKeyVaultClient{}, quietTestLogger())
	raw, _ := json.Marshal(azurekv.Config{
		VaultURL:        "https://test-vault.vault.azure.net",
		CertificateName: "demo",
		Tags:            map[string]string{"certctl-managed-by": "spoofed"},
	})
	err := c.ValidateConfig(ctx, raw)
	if err == nil || !strings.Contains(err.Error(), "reserved provenance key") {
		t.Errorf("expected reserved-key rejection; got %v", err)
	}
}

func TestAzureKV_DeployCertificate_FreshImport(t *testing.T) {
	ctx := context.Background()
	certPEM, keyPEM, derBytes, serial := generateTestCert(t, "fresh.example.com")

	mock := &mockKeyVaultClient{
		// Snapshot read returns NotFound-equivalent (empty CER).
		// Post-verify returns the CER bytes of the cert we just imported.
		getOutput: &azurekv.GetCertificateOutput{
			VersionID: "01234567890abcdef01234567890abcd",
			Serial:    serial,
			CERBytes:  derBytes,
		},
	}
	cfg := &azurekv.Config{
		VaultURL:        "https://test-vault.vault.azure.net",
		CertificateName: "fresh-cert",
	}
	c := azurekv.NewWithClient(cfg, mock, quietTestLogger())

	res, err := c.DeployCertificate(ctx, target.DeploymentRequest{
		CertPEM:  certPEM,
		KeyPEM:   keyPEM,
		Metadata: map[string]string{"certificate_id": "mc-fresh"},
	})
	if err != nil {
		t.Fatalf("DeployCertificate: %v", err)
	}
	if !res.Success {
		t.Errorf("expected Success=true")
	}
	if res.DeploymentID == "" {
		t.Errorf("expected version ID populated")
	}
	if !strings.HasPrefix(res.TargetAddress, "https://test-vault.vault.azure.net/certificates/fresh-cert/") {
		t.Errorf("expected KID URI in TargetAddress; got %s", res.TargetAddress)
	}
	if len(mock.importCalls) != 1 {
		t.Errorf("expected exactly 1 ImportCertificate call; got %d", len(mock.importCalls))
	}
	// PFX is the wire format; assert the import call carries non-empty PFX bytes.
	if mock.importCalls[0].PFXBase64 == "" {
		t.Error("expected PFXBase64 populated on import call")
	}
	// Provenance tags applied.
	if mock.importCalls[0].Tags["certctl-managed-by"] != "certctl" {
		t.Error("expected certctl-managed-by=certctl provenance tag")
	}
	if mock.importCalls[0].Tags["certctl-certificate-id"] != "mc-fresh" {
		t.Error("expected certctl-certificate-id provenance tag")
	}
}

func TestAzureKV_DeployCertificate_RollbackOnSerialMismatch(t *testing.T) {
	ctx := context.Background()
	certPEM, keyPEM, _, _ := generateTestCert(t, "mismatch.example.com")
	_, _, snapshotDER, _ := generateTestCert(t, "snapshot.example.com")

	// Snapshot returns previous version's bytes; post-verify returns
	// a serial that doesn't match the cert we just imported → trigger
	// rollback.
	mock := &mockKeyVaultClient{
		getOutput: &azurekv.GetCertificateOutput{
			Serial:   "ff:ff:ff:ff",
			CERBytes: snapshotDER,
		},
	}
	cfg := &azurekv.Config{
		VaultURL:        "https://test-vault.vault.azure.net",
		CertificateName: "rollback-cert",
	}
	c := azurekv.NewWithClient(cfg, mock, quietTestLogger())

	_, err := c.DeployCertificate(ctx, target.DeploymentRequest{
		CertPEM:  certPEM,
		KeyPEM:   keyPEM,
		Metadata: map[string]string{"certificate_id": "mc-rollback"},
	})
	if err == nil {
		t.Fatal("expected error on serial mismatch")
	}
	if !strings.Contains(err.Error(), "rolled back") {
		t.Errorf("expected error to mention rollback; got %v", err)
	}
	// Two import calls: initial + rollback.
	if len(mock.importCalls) != 2 {
		t.Errorf("expected 2 import calls (initial + rollback); got %d", len(mock.importCalls))
	}
}

func TestAzureKV_DeployCertificate_EmptyKey(t *testing.T) {
	ctx := context.Background()
	certPEM, _, _, _ := generateTestCert(t, "nokey.example.com")
	cfg := &azurekv.Config{
		VaultURL:        "https://test-vault.vault.azure.net",
		CertificateName: "demo",
	}
	c := azurekv.NewWithClient(cfg, &mockKeyVaultClient{}, quietTestLogger())
	_, err := c.DeployCertificate(ctx, target.DeploymentRequest{
		CertPEM: certPEM,
		KeyPEM:  "",
	})
	if err == nil || !strings.Contains(err.Error(), "key_pem is required") {
		t.Errorf("expected key-required error; got %v", err)
	}
}

func TestAzureKV_DeployCertificate_NoClient(t *testing.T) {
	ctx := context.Background()
	cfg := &azurekv.Config{
		VaultURL:        "https://test-vault.vault.azure.net",
		CertificateName: "demo",
	}
	c := azurekv.NewWithClient(cfg, nil, quietTestLogger())
	_, err := c.DeployCertificate(ctx, target.DeploymentRequest{CertPEM: "x", KeyPEM: "y"})
	if err == nil || !strings.Contains(err.Error(), "client not initialized") {
		t.Errorf("expected client-not-initialized; got %v", err)
	}
}

func TestAzureKV_ValidateOnly_NotSupported(t *testing.T) {
	ctx := context.Background()
	c := azurekv.NewWithClient(&azurekv.Config{
		VaultURL:        "https://test-vault.vault.azure.net",
		CertificateName: "demo",
	}, &mockKeyVaultClient{}, quietTestLogger())
	err := c.ValidateOnly(ctx, target.DeploymentRequest{})
	if !errors.Is(err, target.ErrValidateOnlyNotSupported) {
		t.Errorf("expected ErrValidateOnlyNotSupported; got %v", err)
	}
}

func TestAzureKV_ValidateDeployment_SerialMatch(t *testing.T) {
	ctx := context.Background()
	mock := &mockKeyVaultClient{
		getOutput: &azurekv.GetCertificateOutput{Serial: "ab:cd:01"},
	}
	cfg := &azurekv.Config{
		VaultURL:        "https://test-vault.vault.azure.net",
		CertificateName: "demo",
	}
	c := azurekv.NewWithClient(cfg, mock, quietTestLogger())
	res, err := c.ValidateDeployment(ctx, target.ValidationRequest{Serial: "ab:cd:01"})
	if err != nil {
		t.Fatalf("ValidateDeployment: %v", err)
	}
	if !res.Valid {
		t.Errorf("expected Valid=true; got %+v", res)
	}
}

func TestAzureKV_ValidateDeployment_SerialMismatch(t *testing.T) {
	ctx := context.Background()
	mock := &mockKeyVaultClient{
		getOutput: &azurekv.GetCertificateOutput{Serial: "00:00"},
	}
	cfg := &azurekv.Config{
		VaultURL:        "https://test-vault.vault.azure.net",
		CertificateName: "demo",
	}
	c := azurekv.NewWithClient(cfg, mock, quietTestLogger())
	res, err := c.ValidateDeployment(ctx, target.ValidationRequest{Serial: "ab:cd:01"})
	if err != nil {
		t.Fatalf("ValidateDeployment: %v", err)
	}
	if res.Valid {
		t.Errorf("expected Valid=false on serial mismatch; got %+v", res)
	}
}

// TestAzureKV_DeployCertificate_AzureSDKError_Surfaced pins that an
// underlying SDK error from the SDK client (e.g. 403 Forbidden, 404
// NotFound, 429 Throttled) bubbles up through the connector's wrap
// layer cleanly. Mirrors the AWS ACM failure-test shape — the Azure
// SDK uses azcore.ResponseError as its typed-error shape but Key
// Vault wraps it as a generic error in many cases; we test the
// generic-error wrap chain rather than reach into azcore.
func TestAzureKV_DeployCertificate_AzureSDKError_Surfaced(t *testing.T) {
	ctx := context.Background()
	certPEM, keyPEM, _, _ := generateTestCert(t, "sdkerr.example.com")

	mock := &mockKeyVaultClient{
		importErr: errors.New("azcertificates: 403 Forbidden — caller does not have certificates/import permission"),
	}
	cfg := &azurekv.Config{
		VaultURL:        "https://test-vault.vault.azure.net",
		CertificateName: "sdkerr",
	}
	c := azurekv.NewWithClient(cfg, mock, quietTestLogger())
	_, err := c.DeployCertificate(ctx, target.DeploymentRequest{
		CertPEM: certPEM, KeyPEM: keyPEM,
		Metadata: map[string]string{"certificate_id": "mc-sdkerr"},
	})
	if err == nil {
		t.Fatal("expected SDK error")
	}
	if !strings.Contains(err.Error(), "ImportCertificate failed") {
		t.Errorf("expected adapter wrap framing; got %v", err)
	}
	if !strings.Contains(err.Error(), "Forbidden") {
		t.Errorf("expected SDK error substring preserved; got %v", err)
	}
}
