package postfix_test

import (
	"context"
	"encoding/json"
	"log/slog"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/shankar0123/certctl/internal/connector/target"
	"github.com/shankar0123/certctl/internal/connector/target/postfix"
)

// --- Config Validation Tests ---

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

	tmpDir := t.TempDir()
	cfg := postfix.Config{
		Mode:            "postfix",
		CertPath:        filepath.Join(tmpDir, "cert.pem"),
		KeyPath:         filepath.Join(tmpDir, "key.pem"),
		ChainPath:       filepath.Join(tmpDir, "chain.pem"),
		ReloadCommand:   "true",
		ValidateCommand: "true",
	}

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

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

	tmpDir := t.TempDir()
	cfg := postfix.Config{
		Mode:            "dovecot",
		CertPath:        filepath.Join(tmpDir, "cert.pem"),
		KeyPath:         filepath.Join(tmpDir, "key.pem"),
		ReloadCommand:   "true",
		ValidateCommand: "true",
	}

	connector := postfix.New(&cfg, logger)
	rawConfig, _ := json.Marshal(cfg)
	err := connector.ValidateConfig(ctx, rawConfig)
	if err != nil {
		t.Fatalf("ValidateConfig for dovecot mode failed: %v", err)
	}
}

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

	connector := postfix.New(&postfix.Config{}, logger)
	err := connector.ValidateConfig(ctx, json.RawMessage(`{invalid}`))
	if err == nil {
		t.Fatal("expected error for invalid JSON")
	}
}

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

	cfg := postfix.Config{
		Mode:          "nginx",
		CertPath:      "/tmp/cert.pem",
		ReloadCommand: "true",
	}

	connector := postfix.New(&cfg, logger)
	rawConfig, _ := json.Marshal(cfg)
	err := connector.ValidateConfig(ctx, rawConfig)
	if err == nil {
		t.Fatal("expected error for invalid mode")
	}
	if !strings.Contains(err.Error(), "invalid mode") {
		t.Fatalf("expected 'invalid mode' error, got: %v", err)
	}
}

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

	cfg := postfix.Config{
		Mode:            "postfix",
		CertPath:        "/nonexistent/directory/cert.pem",
		KeyPath:         "/nonexistent/directory/key.pem",
		ReloadCommand:   "true",
		ValidateCommand: "true",
	}

	connector := postfix.New(&cfg, logger)
	rawConfig, _ := json.Marshal(cfg)
	err := connector.ValidateConfig(ctx, rawConfig)
	if err == nil {
		t.Fatal("expected error for non-existent cert directory")
	}
}

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

	// An empty config with mode=postfix will get defaults applied.
	// The defaults point to /etc/postfix/certs/ which won't exist in test,
	// so this will fail at directory check — which is fine; it validates that
	// defaults are applied and path validation catches missing dirs.
	cfg := postfix.Config{
		Mode:            "postfix",
		ReloadCommand:   "true",
		ValidateCommand: "true",
	}

	connector := postfix.New(&cfg, logger)
	rawConfig, _ := json.Marshal(cfg)
	err := connector.ValidateConfig(ctx, rawConfig)
	if err == nil {
		t.Fatal("expected error when default cert directory doesn't exist")
	}
}

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

	// Create a directory matching the postfix default path structure
	tmpDir := t.TempDir()
	certDir := filepath.Join(tmpDir, "postfix", "certs")
	os.MkdirAll(certDir, 0755)

	cfg := postfix.Config{
		Mode:     "postfix",
		CertPath: filepath.Join(certDir, "cert.pem"),
		KeyPath:  filepath.Join(certDir, "key.pem"),
		// Leave ReloadCommand and ValidateCommand empty to get defaults
	}

	connector := postfix.New(&cfg, logger)
	rawConfig, _ := json.Marshal(cfg)

	// Defaults will be applied for reload/validate commands.
	// The validate command will be "postfix check" which won't exist in test env
	// but ValidateConfig only warns on validate command failure (doesn't error).
	// The reload command "postfix reload" will be validated by ValidateShellCommand.
	err := connector.ValidateConfig(ctx, rawConfig)
	if err != nil {
		t.Fatalf("ValidateConfig with defaults failed: %v", err)
	}
}

// --- Deployment Tests ---

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

	tmpDir := t.TempDir()
	cfg := &postfix.Config{
		Mode:            "postfix",
		CertPath:        filepath.Join(tmpDir, "cert.pem"),
		KeyPath:         filepath.Join(tmpDir, "key.pem"),
		ChainPath:       filepath.Join(tmpDir, "chain.pem"),
		ReloadCommand:   "true",
		ValidateCommand: "true",
	}

	connector := postfix.New(cfg, logger)

	req := target.DeploymentRequest{
		CertPEM:  "-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----",
		KeyPEM:   "-----BEGIN PRIVATE KEY-----\nkey\n-----END PRIVATE KEY-----",
		ChainPEM: "-----BEGIN CERTIFICATE-----\nchain\n-----END CERTIFICATE-----",
	}

	result, err := connector.DeployCertificate(ctx, req)
	if err != nil {
		t.Fatalf("DeployCertificate failed: %v", err)
	}
	if !result.Success {
		t.Fatalf("expected success, got: %s", result.Message)
	}

	// Verify cert file was written (just cert, not chain — since chain_path is set)
	certData, err := os.ReadFile(cfg.CertPath)
	if err != nil {
		t.Fatalf("failed to read cert file: %v", err)
	}
	if string(certData) != req.CertPEM {
		t.Errorf("cert content mismatch: got %q", string(certData))
	}

	// Verify key file was written
	keyData, err := os.ReadFile(cfg.KeyPath)
	if err != nil {
		t.Fatalf("failed to read key file: %v", err)
	}
	if string(keyData) != req.KeyPEM {
		t.Errorf("key content mismatch")
	}

	// Verify chain file was written
	chainData, err := os.ReadFile(cfg.ChainPath)
	if err != nil {
		t.Fatalf("failed to read chain file: %v", err)
	}
	if string(chainData) != req.ChainPEM {
		t.Errorf("chain content mismatch")
	}

	// Verify cert has correct permissions (0644)
	info, err := os.Stat(cfg.CertPath)
	if err != nil {
		t.Fatalf("failed to stat cert file: %v", err)
	}
	if info.Mode().Perm() != 0644 {
		t.Errorf("expected cert permissions 0644, got %v", info.Mode().Perm())
	}

	// Verify key has correct permissions (0600)
	info, err = os.Stat(cfg.KeyPath)
	if err != nil {
		t.Fatalf("failed to stat key file: %v", err)
	}
	if info.Mode().Perm() != 0600 {
		t.Errorf("expected key permissions 0600, got %v", info.Mode().Perm())
	}

	// Verify metadata
	if result.Metadata == nil {
		t.Fatal("expected metadata in result")
	}
	if result.Metadata["cert_path"] != cfg.CertPath {
		t.Errorf("expected cert_path in metadata")
	}
	if result.Metadata["mode"] != "postfix" {
		t.Errorf("expected mode=postfix in metadata, got %s", result.Metadata["mode"])
	}
	if _, ok := result.Metadata["duration_ms"]; !ok {
		t.Errorf("expected duration_ms in metadata")
	}
}

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

	tmpDir := t.TempDir()
	cfg := &postfix.Config{
		Mode:            "postfix",
		CertPath:        filepath.Join(tmpDir, "cert.pem"),
		KeyPath:         filepath.Join(tmpDir, "key.pem"),
		ChainPath:       "", // No chain_path — chain should be appended to cert
		ReloadCommand:   "true",
		ValidateCommand: "true",
	}

	connector := postfix.New(cfg, logger)

	certPEM := "-----BEGIN CERTIFICATE-----\ncert\n-----END CERTIFICATE-----"
	chainPEM := "-----BEGIN CERTIFICATE-----\nchain\n-----END CERTIFICATE-----"

	req := target.DeploymentRequest{
		CertPEM:  certPEM,
		KeyPEM:   "-----BEGIN PRIVATE KEY-----\nkey\n-----END PRIVATE KEY-----",
		ChainPEM: chainPEM,
	}

	result, err := connector.DeployCertificate(ctx, req)
	if err != nil {
		t.Fatalf("DeployCertificate failed: %v", err)
	}
	if !result.Success {
		t.Fatalf("expected success, got: %s", result.Message)
	}

	// Verify cert file contains both cert and chain (fullchain)
	certData, err := os.ReadFile(cfg.CertPath)
	if err != nil {
		t.Fatalf("failed to read cert file: %v", err)
	}
	expected := certPEM + "\n" + chainPEM
	if string(certData) != expected {
		t.Errorf("expected fullchain content, got: %q", string(certData))
	}
}

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

	cfg := &postfix.Config{
		Mode:            "postfix",
		CertPath:        "/nonexistent/directory/cert.pem",
		KeyPath:         "/nonexistent/directory/key.pem",
		ReloadCommand:   "true",
		ValidateCommand: "true",
	}

	connector := postfix.New(cfg, logger)

	req := target.DeploymentRequest{
		CertPEM:  "cert",
		ChainPEM: "chain",
	}

	result, err := connector.DeployCertificate(ctx, req)
	if err == nil {
		t.Fatal("expected error when cert write fails")
	}
	if result.Success {
		t.Fatal("expected failure result")
	}
}

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

	tmpDir := t.TempDir()
	cfg := &postfix.Config{
		Mode:            "postfix",
		CertPath:        filepath.Join(tmpDir, "cert.pem"),
		KeyPath:         filepath.Join(tmpDir, "key.pem"),
		ReloadCommand:   "true",
		ValidateCommand: "false", // Exits with code 1
	}

	connector := postfix.New(cfg, logger)

	req := target.DeploymentRequest{
		CertPEM:  "cert",
		ChainPEM: "chain",
	}

	result, err := connector.DeployCertificate(ctx, req)
	if err == nil {
		t.Fatal("expected error when validate command fails")
	}
	if result.Success {
		t.Fatal("expected failure result")
	}
}

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

	tmpDir := t.TempDir()
	cfg := &postfix.Config{
		Mode:            "postfix",
		CertPath:        filepath.Join(tmpDir, "cert.pem"),
		KeyPath:         filepath.Join(tmpDir, "key.pem"),
		ReloadCommand:   "false", // Exits with code 1
		ValidateCommand: "true",
	}

	connector := postfix.New(cfg, logger)

	req := target.DeploymentRequest{
		CertPEM:  "cert",
		ChainPEM: "chain",
	}

	result, err := connector.DeployCertificate(ctx, req)
	if err == nil {
		t.Fatal("expected error when reload command fails")
	}
	if result.Success {
		t.Fatal("expected failure result")
	}
}

// --- Validation Tests ---

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

	tmpDir := t.TempDir()
	certPath := filepath.Join(tmpDir, "cert.pem")
	os.WriteFile(certPath, []byte("cert"), 0644)

	cfg := &postfix.Config{
		Mode:            "postfix",
		CertPath:        certPath,
		ValidateCommand: "true",
	}

	connector := postfix.New(cfg, logger)

	result, err := connector.ValidateDeployment(ctx, target.ValidationRequest{
		CertificateID: "mc-test",
		Serial:        "123",
	})
	if err != nil {
		t.Fatalf("ValidateDeployment failed: %v", err)
	}
	if !result.Valid {
		t.Fatal("expected valid deployment")
	}
	if result.Metadata == nil {
		t.Fatal("expected metadata in result")
	}
	if result.Metadata["mode"] != "postfix" {
		t.Errorf("expected mode=postfix in metadata")
	}
}

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

	cfg := &postfix.Config{
		Mode:            "postfix",
		CertPath:        "/nonexistent/cert.pem",
		ValidateCommand: "true",
	}

	connector := postfix.New(cfg, logger)

	result, err := connector.ValidateDeployment(ctx, target.ValidationRequest{
		CertificateID: "mc-test",
		Serial:        "123",
	})
	if err == nil {
		t.Fatal("expected error for missing cert file")
	}
	if result.Valid {
		t.Fatal("expected invalid result")
	}
}

// --- Security Tests (Command Injection Prevention) ---

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

	tmpDir := t.TempDir()
	cfg := postfix.Config{
		Mode:            "postfix",
		CertPath:        filepath.Join(tmpDir, "cert.pem"),
		KeyPath:         filepath.Join(tmpDir, "key.pem"),
		ReloadCommand:   "postfix reload; rm -rf /",
		ValidateCommand: "true",
	}

	connector := postfix.New(&cfg, logger)
	rawConfig, _ := json.Marshal(cfg)
	err := connector.ValidateConfig(ctx, rawConfig)
	if err == nil {
		t.Fatal("expected error for command injection in reload_command")
	}
}

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

	tmpDir := t.TempDir()
	cfg := postfix.Config{
		Mode:            "postfix",
		CertPath:        filepath.Join(tmpDir, "cert.pem"),
		KeyPath:         filepath.Join(tmpDir, "key.pem"),
		ReloadCommand:   "true",
		ValidateCommand: "postfix check | cat /etc/passwd",
	}

	connector := postfix.New(&cfg, logger)
	rawConfig, _ := json.Marshal(cfg)
	err := connector.ValidateConfig(ctx, rawConfig)
	if err == nil {
		t.Fatal("expected error for command injection in validate_command")
	}
}

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

	tmpDir := t.TempDir()
	cfg := postfix.Config{
		Mode:            "postfix",
		CertPath:        filepath.Join(tmpDir, "cert.pem"),
		KeyPath:         filepath.Join(tmpDir, "key.pem"),
		ReloadCommand:   "echo $(whoami)",
		ValidateCommand: "true",
	}

	connector := postfix.New(&cfg, logger)
	rawConfig, _ := json.Marshal(cfg)
	err := connector.ValidateConfig(ctx, rawConfig)
	if err == nil {
		t.Fatal("expected error for command substitution in reload_command")
	}
}

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

	tmpDir := t.TempDir()
	cfg := postfix.Config{
		Mode:            "postfix",
		CertPath:        filepath.Join(tmpDir, "cert.pem"),
		KeyPath:         filepath.Join(tmpDir, "key.pem"),
		ReloadCommand:   "true",
		ValidateCommand: "postfix check `whoami`",
	}

	connector := postfix.New(&cfg, logger)
	rawConfig, _ := json.Marshal(cfg)
	err := connector.ValidateConfig(ctx, rawConfig)
	if err == nil {
		t.Fatal("expected error for backtick injection in validate_command")
	}
}
