package main

import (
	"bytes"
	"context"
	"encoding/base64"
	"fmt"
	"net"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"go.uber.org/zap"
	"go.uber.org/zap/zaptest"
	"go.uber.org/zap/zaptest/observer"

	"iop/apps/control-plane/internal/credentialseal"
)

// writeKeyManifest writes a valid 0600 key manifest with one 32-byte key
// (id "primary", version 1) and returns its path. The material is deterministic
// so a reopened keyring holds identical bytes.
func writeKeyManifest(t *testing.T, fill byte) string {
	t.Helper()
	path := filepath.Join(t.TempDir(), "keyring.yaml")
	material := base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{fill}, 32))
	body := "keys:\n  - id: \"primary\"\n    version: 1\n    material: \"" + material + "\"\n"
	if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
		t.Fatalf("write manifest: %v", err)
	}
	return path
}

func encryptionConfig(keyFile string) credentialseal.FileConfig {
	return credentialseal.FileConfig{KeyFile: keyFile, ActiveKeyID: "primary", ActiveKeyVersion: 1}
}

func TestComposeCredentialRuntimeDatabaseOnly(t *testing.T) {
	cfg := defaultConfig()
	cfg.Database.URL = ""
	cred, err := composeCredentialRuntime(context.Background(), cfg, zaptest.NewLogger(t))
	if err != nil {
		t.Fatalf("compose: %v", err)
	}
	if cred.store != nil {
		t.Fatal("empty database URL must leave the store unopened")
	}
	if cred.service != nil {
		t.Fatal("no service without encryption")
	}
}

func TestComposeCredentialRuntimeSecretDisabledMode(t *testing.T) {
	cfg := defaultConfig()
	cfg.Database.URL = filepath.Join(t.TempDir(), "disabled.db")
	cred, err := composeCredentialRuntime(context.Background(), cfg, zaptest.NewLogger(t))
	if err != nil {
		t.Fatalf("compose: %v", err)
	}
	if cred.store == nil {
		t.Fatal("store must open for a configured database")
	}
	t.Cleanup(func() { _ = cred.store.Close() })
	if cred.service != nil {
		t.Fatal("secret-disabled mode must not inject a credential service")
	}
}

func TestComposeCredentialRuntimeConfiguredInjectsService(t *testing.T) {
	cfg := defaultConfig()
	cfg.Database.URL = filepath.Join(t.TempDir(), "configured.db")
	cfg.CredentialEncryption = encryptionConfig(writeKeyManifest(t, 0x3C))

	cred, err := composeCredentialRuntime(context.Background(), cfg, zaptest.NewLogger(t))
	if err != nil {
		t.Fatalf("compose: %v", err)
	}
	if cred.store == nil {
		t.Fatal("store must open when encryption is configured")
	}
	t.Cleanup(func() { _ = cred.store.Close() })
	if cred.service == nil {
		t.Fatal("configured encryption must inject a credential service")
	}
}

func TestComposeCredentialRuntimePartialConfigFailsBeforeStore(t *testing.T) {
	for name, enc := range map[string]credentialseal.FileConfig{
		"only-file":    {KeyFile: "/run/secrets/x"},
		"only-id":      {ActiveKeyID: "primary"},
		"only-version": {ActiveKeyVersion: 1},
		"file-id":      {KeyFile: "/run/secrets/x", ActiveKeyID: "primary"},
	} {
		t.Run(name, func(t *testing.T) {
			cfg := defaultConfig()
			dbPath := filepath.Join(t.TempDir(), "partial.db")
			cfg.Database.URL = dbPath
			cfg.CredentialEncryption = enc

			_, err := composeCredentialRuntime(context.Background(), cfg, zaptest.NewLogger(t))
			if err == nil {
				t.Fatal("expected partial encryption config to fail")
			}
			if !strings.Contains(err.Error(), "credential encryption") {
				t.Fatalf("expected credential encryption error, got: %v", err)
			}
			// Fail-closed before opening the store: no database file is created.
			if _, statErr := os.Stat(dbPath); statErr == nil {
				t.Fatal("store must not open when encryption config is partial")
			}
		})
	}
}

func TestComposeCredentialRuntimeInvalidManifestVariants(t *testing.T) {
	t.Run("missing file", func(t *testing.T) {
		cfg := defaultConfig()
		cfg.Database.URL = filepath.Join(t.TempDir(), "x.db")
		cfg.CredentialEncryption = encryptionConfig(filepath.Join(t.TempDir(), "absent.yaml"))
		if _, err := composeCredentialRuntime(context.Background(), cfg, zaptest.NewLogger(t)); err == nil {
			t.Fatal("expected missing key file to fail")
		}
	})

	t.Run("insecure permissions", func(t *testing.T) {
		path := filepath.Join(t.TempDir(), "perm.yaml")
		material := base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{0x11}, 32))
		body := "keys:\n  - id: \"primary\"\n    version: 1\n    material: \"" + material + "\"\n"
		if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
			t.Fatalf("write manifest: %v", err)
		}
		cfg := defaultConfig()
		cfg.Database.URL = filepath.Join(t.TempDir(), "x.db")
		cfg.CredentialEncryption = encryptionConfig(path)
		if _, err := composeCredentialRuntime(context.Background(), cfg, zaptest.NewLogger(t)); err == nil {
			t.Fatal("expected group-readable key file to fail")
		}
	})

	t.Run("malformed manifest", func(t *testing.T) {
		path := filepath.Join(t.TempDir(), "bad.yaml")
		if err := os.WriteFile(path, []byte("keys: [broken"), 0o600); err != nil {
			t.Fatalf("write manifest: %v", err)
		}
		cfg := defaultConfig()
		cfg.Database.URL = filepath.Join(t.TempDir(), "x.db")
		cfg.CredentialEncryption = encryptionConfig(path)
		if _, err := composeCredentialRuntime(context.Background(), cfg, zaptest.NewLogger(t)); err == nil {
			t.Fatal("expected malformed manifest to fail")
		}
	})

	t.Run("unknown active key", func(t *testing.T) {
		cfg := defaultConfig()
		cfg.Database.URL = filepath.Join(t.TempDir(), "x.db")
		cfg.CredentialEncryption = credentialseal.FileConfig{KeyFile: writeKeyManifest(t, 0x22), ActiveKeyID: "primary", ActiveKeyVersion: 9}
		if _, err := composeCredentialRuntime(context.Background(), cfg, zaptest.NewLogger(t)); err == nil {
			t.Fatal("expected unknown active key to fail")
		}
	})
}

func TestComposeCredentialRuntimeRestartReopen(t *testing.T) {
	dbPath := filepath.Join(t.TempDir(), "restart.db")
	keyFile := writeKeyManifest(t, 0x4D)
	cfg := defaultConfig()
	cfg.Database.URL = dbPath
	cfg.CredentialEncryption = encryptionConfig(keyFile)

	first, err := composeCredentialRuntime(context.Background(), cfg, zaptest.NewLogger(t))
	if err != nil {
		t.Fatalf("first compose: %v", err)
	}
	if err := first.store.Close(); err != nil {
		t.Fatalf("close first store: %v", err)
	}

	second, err := composeCredentialRuntime(context.Background(), cfg, zaptest.NewLogger(t))
	if err != nil {
		t.Fatalf("second compose after restart: %v", err)
	}
	t.Cleanup(func() { _ = second.store.Close() })
	if second.store == nil || second.service == nil {
		t.Fatal("restart must reopen store and reinject service")
	}
}

func TestRunWithEncryptionConfigStartsAndLogsNoSecret(t *testing.T) {
	t.Setenv("IOP_DATABASE_URL", "")
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()

	core, observed := observer.New(zap.InfoLevel)
	logger := zap.New(core)

	keyFile := writeKeyManifest(t, 0x5E)
	material := readManifestMaterial(t, keyFile)

	cfg := defaultConfig()
	cfg.Database.URL = filepath.Join(t.TempDir(), "run-enc.db")
	cfg.CredentialEncryption = encryptionConfig(keyFile)
	cfg.Server.Listen = "127.0.0.1:0"
	cfg.Server.WireListen = "127.0.0.1:0"
	cfg.Server.EdgeWireListen = "127.0.0.1:0"

	errCh := make(chan error, 1)
	go func() { errCh <- run(ctx, cfg, logger) }()

	select {
	case err := <-errCh:
		if err != nil && ctx.Err() == nil {
			t.Fatalf("run with encryption: %v", err)
		}
	case <-time.After(1 * time.Second):
		cancel()
		<-errCh
	}

	var sawReady, encEnabled bool
	for _, entry := range observed.All() {
		if entry.Message == "control-plane credential store ready" {
			sawReady = true
			for _, f := range entry.Context {
				if f.Key == "secret_encryption" && f.Integer == 1 {
					encEnabled = true
				}
			}
		}
		// No log entry may carry the raw key material.
		full := entry.Message
		for _, f := range entry.Context {
			full += fmt.Sprintf(" %s=%v", f.Key, f.Interface) + f.String
		}
		if strings.Contains(full, material) {
			t.Fatalf("log leaked key material: %q", entry.Message)
		}
	}
	if !sawReady {
		t.Fatal("expected credential store ready log")
	}
	if !encEnabled {
		t.Fatal("expected secret_encryption=true in ready log")
	}
}

func TestRunWithPartialEncryptionConfigDoesNotStartListener(t *testing.T) {
	t.Setenv("IOP_DATABASE_URL", "")
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()

	core, observed := observer.New(zap.InfoLevel)
	logger := zap.New(core)

	cfg := defaultConfig()
	cfg.Database.URL = filepath.Join(t.TempDir(), "partial-run.db")
	cfg.CredentialEncryption = credentialseal.FileConfig{ActiveKeyID: "primary", ActiveKeyVersion: 1} // no key_file
	cfg.Server.Listen = "127.0.0.1:0"
	cfg.Server.WireListen = "127.0.0.1:0"
	cfg.Server.EdgeWireListen = "127.0.0.1:0"

	errCh := make(chan error, 1)
	go func() { errCh <- run(ctx, cfg, logger) }()

	select {
	case err := <-errCh:
		if err == nil {
			t.Fatal("expected run to fail on partial encryption config")
		}
		if !strings.Contains(err.Error(), "credential encryption") {
			t.Fatalf("expected credential encryption error, got: %v", err)
		}
	case <-time.After(2 * time.Second):
		cancel()
		t.Fatal("run did not fail within timeout")
	}

	for _, entry := range observed.All() {
		if strings.Contains(entry.Message, "http endpoint listening") {
			t.Fatal("http listener must not start when encryption config is partial")
		}
	}
}

func readManifestMaterial(t *testing.T, path string) string {
	t.Helper()
	data, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("read manifest: %v", err)
	}
	for _, line := range strings.Split(string(data), "\n") {
		line = strings.TrimSpace(line)
		if strings.HasPrefix(line, "material:") {
			return strings.Trim(strings.TrimSpace(strings.TrimPrefix(line, "material:")), "\"")
		}
	}
	t.Fatal("manifest material not found")
	return ""
}

func TestRunAllowsUnconfiguredDatabase(t *testing.T) {
	t.Setenv("IOP_DATABASE_URL", "")
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()

	core, observed := observer.New(zap.InfoLevel)
	logger := zap.New(core)

	cfg := defaultConfig()
	cfg.Database.URL = ""
	cfg.Server.Listen = "127.0.0.1:0"
	cfg.Server.WireListen = "127.0.0.1:0"
	cfg.Server.EdgeWireListen = "127.0.0.1:0"

	errCh := make(chan error, 1)
	go func() {
		errCh <- run(ctx, cfg, logger)
	}()

	// Give the server a moment to start (or fail).
	select {
	case err := <-errCh:
		// Server exited; check it did not fail on empty database.
		if err != nil && !strings.Contains(err.Error(), "use of closed") && !strings.Contains(err.Error(), "server closed") {
			// Accept context cancellation or shutdown errors.
			if ctx.Err() == nil {
				t.Fatalf("run with empty database: %v", err)
			}
		}
	case <-time.After(1 * time.Second):
		// Server is still running; cancel context to shut it down.
		cancel()
		<-errCh
	}

	// No log entry should reference a database connection attempt.
	for _, entry := range observed.All() {
		if strings.Contains(entry.Message, "credential-store") || strings.Contains(entry.Message, "credentialstore") {
			t.Fatalf("unexpected credential store log: %q", entry.Message)
		}
	}
}

func TestRunRejectsInvalidDatabaseURL(t *testing.T) {
	t.Setenv("IOP_DATABASE_URL", "")
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()

	core, observed := observer.New(zap.InfoLevel)
	logger := zap.New(core)

	cfg := defaultConfig()
	// Use an unsupported scheme that dialectFromURL rejects immediately.
	cfg.Database.URL = "memdb://invalid-host:99999/test"
	cfg.Server.Listen = "127.0.0.1:0"
	cfg.Server.WireListen = "127.0.0.1:0"
	cfg.Server.EdgeWireListen = "127.0.0.1:0"

	errCh := make(chan error, 1)
	go func() {
		errCh <- run(ctx, cfg, logger)
	}()

	select {
	case err := <-errCh:
		if err == nil {
			t.Fatal("expected run to fail with invalid database URL")
		}
		if !strings.Contains(err.Error(), "credential store") && !strings.Contains(err.Error(), "credentialstore") {
			t.Fatalf("expected credential store error, got: %v", err)
		}
	case <-time.After(2 * time.Second):
		cancel()
		t.Fatal("run did not fail within timeout")
	}

	// Verify no credential store log contains the raw URL.
	for _, entry := range observed.All() {
		if strings.Contains(entry.Message, "memdb://") {
			t.Fatalf("raw invalid URL leaked into log: %q", entry.Message)
		}
	}
}

func TestRunWithSQLiteFileSucceeds(t *testing.T) {
	t.Setenv("IOP_DATABASE_URL", "")
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()

	core, observed := observer.New(zap.InfoLevel)
	logger := zap.New(core)

	tmpDir := t.TempDir()
	dbFile := filepath.Join(tmpDir, "test.db")

	cfg := defaultConfig()
	cfg.Database.URL = dbFile
	cfg.Server.Listen = "127.0.0.1:0"
	cfg.Server.WireListen = "127.0.0.1:0"
	cfg.Server.EdgeWireListen = "127.0.0.1:0"

	errCh := make(chan error, 1)
	go func() {
		errCh <- run(ctx, cfg, logger)
	}()

	select {
	case err := <-errCh:
		if err != nil && ctx.Err() == nil {
			t.Fatalf("run with SQLite: %v", err)
		}
	case <-time.After(1 * time.Second):
		cancel()
		<-errCh
	}

	// Verify DB file was created.
	if _, err := os.Stat(dbFile); os.IsNotExist(err) {
		t.Fatal("SQLite database file was not created")
	}

	// Verify schema was migrated.
	for _, entry := range observed.All() {
		if strings.Contains(entry.Message, "memdb://") {
			t.Fatalf("unexpected URL in log: %q", entry.Message)
		}
	}
}

func TestDatabaseLogFieldsDoNotExposeCredentialStoreURL(t *testing.T) {
	// Verify the existing databaseLogFields helper does not leak credentials
	// when called with a credential store URL.
	fields := databaseLogFields("postgres://admin:supersecret@db:5432/iop-creds?sslmode=require")
	for _, field := range fields {
		if strings.Contains(fmt.Sprint(field), "supersecret") {
			t.Fatalf("credential leaked into log field: %v", field)
		}
	}
}

func TestRunWithUnreachablePostgresFails(t *testing.T) {
	t.Setenv("IOP_DATABASE_URL", "")
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()

	core, observed := observer.New(zap.InfoLevel)
	logger := zap.New(core)

	cfg := defaultConfig()
	// Use a port that is guaranteed to be unreachable.
	cfg.Database.URL = "postgres://localhost:1/noexist"
	cfg.Server.Listen = "127.0.0.1:0"
	cfg.Server.WireListen = "127.0.0.1:0"
	cfg.Server.EdgeWireListen = "127.0.0.1:0"

	errCh := make(chan error, 1)
	go func() {
		errCh <- run(ctx, cfg, logger)
	}()

	select {
	case err := <-errCh:
		if err == nil {
			t.Fatal("expected run to fail with unreachable postgres")
		}
		if !strings.Contains(err.Error(), "credential store") && !strings.Contains(err.Error(), "credentialstore") {
			t.Fatalf("expected credential store error, got: %v", err)
		}
	case <-time.After(2 * time.Second):
		cancel()
		t.Fatal("run did not fail within timeout")
	}

	// Verify no credential store log leaked the URL.
	for _, entry := range observed.All() {
		msg := entry.Message
		for _, f := range entry.Context {
			msg += fmt.Sprintf(" %s=%v", f.Key, f.Interface)
		}
		if strings.Contains(msg, "localhost:1/noexist") {
			t.Fatalf("raw database URL leaked into log: %q", entry.Message)
		}
	}
}

// helper: find a free port for tests.
func TestComposeCredentialRuntimeEncryptionRequiresDatabase(t *testing.T) {
	for _, dbURL := range []string{"", " ", "  \t  "} {
		name := "empty"
		if dbURL == " " {
			name = "whitespace"
		} else if dbURL == "  \t  " {
			name = "spaces-and-tabs"
		}
		t.Run(name, func(t *testing.T) {
			cfg := defaultConfig()
			cfg.Database.URL = dbURL
			cfg.CredentialEncryption = encryptionConfig(writeKeyManifest(t, 0x6F))

			_, err := composeCredentialRuntime(context.Background(), cfg, zaptest.NewLogger(t))
			if err == nil {
				t.Fatal("expected encryption without database to fail")
			}
			if !strings.Contains(err.Error(), "credential encryption requires database.url") {
				t.Fatalf("expected credential encryption requires database.url error, got: %v", err)
			}
		})
	}
}

func TestRunWithEncryptionAndNoDatabaseDoesNotStartListener(t *testing.T) {
	t.Setenv("IOP_DATABASE_URL", "")
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()

	core, observed := observer.New(zap.InfoLevel)
	logger := zap.New(core)

	keyFile := writeKeyManifest(t, 0x7A)
	cfg := defaultConfig()
	cfg.Database.URL = ""
	cfg.CredentialEncryption = encryptionConfig(keyFile)
	cfg.Server.Listen = "127.0.0.1:0"
	cfg.Server.WireListen = "127.0.0.1:0"
	cfg.Server.EdgeWireListen = "127.0.0.1:0"

	errCh := make(chan error, 1)
	go func() { errCh <- run(ctx, cfg, logger) }()

	select {
	case err := <-errCh:
		if err == nil {
			t.Fatal("expected run to fail with encryption and no database")
		}
		if !strings.Contains(err.Error(), "credential encryption requires database.url") {
			t.Fatalf("expected credential encryption requires database.url error, got: %v", err)
		}
	case <-time.After(2 * time.Second):
		cancel()
		t.Fatal("run did not fail within timeout")
	}

	// No HTTP, client wire, or edge wire listening log may be emitted.
	for _, entry := range observed.All() {
		if strings.Contains(entry.Message, "http endpoint listening") {
			t.Fatal("http listener must not start when encryption has no database")
		}
		if entry.Message == "starting client wire WS server" {
			t.Fatal("client wire listener must not start when encryption has no database")
		}
		if entry.Message == "starting edge wire TCP server" {
			t.Fatal("edge wire listener must not start when encryption has no database")
		}
	}
}

func freePort(t *testing.T) string {
	t.Helper()
	l, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatalf("find free port: %v", err)
	}
	defer l.Close()
	return l.Addr().String()
}
