package migration

import (
	"context"
	"database/sql"
	"os"
	"path/filepath"
	"testing"

	_ "modernc.org/sqlite"

	"git.cloudinit.dev/coreci/orca/internal/paths"
)

type mockCAImporter struct {
	called   bool
	certPath string
	keyPath  string
	err      error
}

func (m *mockCAImporter) ImportCA(ctx context.Context, caCertPath, caKeyPath string) error {
	m.called = true
	m.certPath = caCertPath
	m.keyPath = caKeyPath
	return m.err
}

// createV08DB creates a real SQLite DB at path with a nodes table that
// has a namespace column (simulating the v0.8 dual-write window). The
// namespace column is what migrateDBSchema should drop.
func createV08DB(t *testing.T, path string) {
	t.Helper()
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		t.Fatalf("mkdir: %v", err)
	}
	db, err := sql.Open("sqlite", path)
	if err != nil {
		t.Fatalf("open sqlite: %v", err)
	}
	defer db.Close()
	if _, err := db.Exec(`CREATE TABLE nodes (
		id TEXT PRIMARY KEY,
		name TEXT NOT NULL,
		address TEXT NOT NULL,
		state TEXT NOT NULL DEFAULT 'pending',
		joined_at DATETIME NOT NULL,
		last_seen DATETIME NOT NULL,
		metadata TEXT,
		namespace TEXT
	)`); err != nil {
		t.Fatalf("create nodes table: %v", err)
	}
	if _, err := db.Exec(`CREATE TABLE audit_log (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		ts DATETIME NOT NULL,
		actor TEXT,
		action TEXT,
		resource TEXT,
		result TEXT
	)`); err != nil {
		t.Fatalf("create audit_log table: %v", err)
	}
	if _, err := db.Exec(`INSERT INTO nodes (id, name, address, state, joined_at, last_seen, namespace) VALUES ('n1','localhost','localhost:8443','ready','2024-01-01','2024-01-01','default')`); err != nil {
		t.Fatalf("insert node: %v", err)
	}
	if _, err := db.Exec(`INSERT INTO audit_log (ts, actor, action, resource, result) VALUES ('2024-01-01','init','init','cluster','success')`); err != nil {
		t.Fatalf("insert audit: %v", err)
	}
}

func setupV08Dir(t *testing.T) string {
	t.Helper()
	dir := t.TempDir()
	createV08DB(t, filepath.Join(dir, "orca.db"))
	mustWrite(t, filepath.Join(dir, "ca.crt"), []byte("fake CA cert"))
	mustWrite(t, filepath.Join(dir, "ca.key"), []byte("fake CA key"))
	return dir
}

func setupV11Dir(t *testing.T) string {
	t.Helper()
	dir := t.TempDir()
	if err := os.MkdirAll(filepath.Join(dir, "cluster"), 0o755); err != nil {
		t.Fatalf("mkdir cluster: %v", err)
	}
	dbDir := filepath.Join(dir, paths.DefaultNamespace(), "db")
	if err := os.MkdirAll(dbDir, 0o755); err != nil {
		t.Fatalf("mkdir defaults/db: %v", err)
	}
	mustWrite(t, filepath.Join(dbDir, "orca.db"), []byte("sqlite v0.11"))
	return dir
}

func mustWrite(t *testing.T, path string, data []byte) {
	t.Helper()
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		t.Fatalf("mkdir %s: %v", filepath.Dir(path), err)
	}
	if err := os.WriteFile(path, data, 0o644); err != nil {
		t.Fatalf("write %s: %v", path, err)
	}
}

func TestDetectv08_DetectsV08Layout(t *testing.T) {
	dir := setupV08Dir(t)
	if !Detectv08(dir) {
		t.Errorf("Detectv08(%q) = false, want true", dir)
	}
}

func TestDetectv08_RejectsV11Layout(t *testing.T) {
	dir := setupV11Dir(t)
	if Detectv08(dir) {
		t.Errorf("Detectv08(%q) = true, want false (v0.11 layout)", dir)
	}
}

func TestDetectv08_EmptyDir(t *testing.T) {
	dir := t.TempDir()
	if Detectv08(dir) {
		t.Errorf("Detectv08 on empty dir = true, want false")
	}
}

func TestDetectv08_EmptyString(t *testing.T) {
	if Detectv08("") {
		t.Errorf("Detectv08(\"\") = true, want false")
	}
}

func TestDetectv08_MissingCA(t *testing.T) {
	dir := t.TempDir()
	createV08DB(t, filepath.Join(dir, "orca.db"))
	if Detectv08(dir) {
		t.Errorf("Detectv08 with db but no CA = true, want false")
	}
}

func TestMigratev08tov11_CreatesV11Layout(t *testing.T) {
	dir := setupV08Dir(t)
	mustWrite(t, filepath.Join(dir, "config.hcl"), []byte("config"))

	if err := Migratev08tov11(MigrateOptions{SourceDir: dir, TargetDir: dir}); err != nil {
		t.Fatalf("Migratev08tov11: %v", err)
	}

	if _, err := os.Stat(filepath.Join(dir, "cluster")); err != nil {
		t.Errorf("cluster/ dir not created: %v", err)
	}
	dbPath := filepath.Join(dir, paths.DefaultNamespace(), "db", "orca.db")
	if _, err := os.Stat(dbPath); err != nil {
		t.Errorf("orca.db not moved to _defaults/db/: %v", err)
	}
	if _, err := os.Stat(filepath.Join(dir, "orca.db")); err == nil {
		t.Errorf("old orca.db still at root (should have been moved)")
	}
	legacyCfg := filepath.Join(dir, "cluster", "config.legacy.hcl")
	if _, err := os.Stat(legacyCfg); err != nil {
		t.Errorf("config.hcl not moved to cluster/config.legacy.hcl: %v", err)
	}
	if _, err := os.Stat(filepath.Join(dir, "config.hcl")); err == nil {
		t.Errorf("old config.hcl still at root (should have been moved)")
	}
	if _, err := os.Stat(filepath.Join(dir, paths.DefaultNamespace(), "ns.md")); err != nil {
		t.Errorf("ns.md not created: %v", err)
	}
	if _, err := os.Stat(filepath.Join(dir, "ca.crt")); err != nil {
		t.Errorf("ca.crt should be preserved, but missing: %v", err)
	}
	if _, err := os.Stat(filepath.Join(dir, "ca.key")); err != nil {
		t.Errorf("ca.key should be preserved, but missing: %v", err)
	}
	if _, err := os.Stat(filepath.Join(dir, paths.DefaultNamespace(), "jobs")); err != nil {
		t.Errorf("jobs/ dir not created: %v", err)
	}
	if _, err := os.Stat(filepath.Join(dir, paths.DefaultNamespace(), "alloc")); err != nil {
		t.Errorf("alloc/ dir not created: %v", err)
	}

	db, err := sql.Open("sqlite", dbPath)
	if err != nil {
		t.Fatalf("open migrated db: %v", err)
	}
	defer db.Close()
	var cols []string
	rows, err := db.Query(`SELECT name FROM pragma_table_info('nodes')`)
	if err != nil {
		t.Fatalf("query table_info: %v", err)
	}
	defer rows.Close()
	for rows.Next() {
		var c string
		if err := rows.Scan(&c); err != nil {
			t.Fatalf("scan: %v", err)
		}
		cols = append(cols, c)
	}
	for _, c := range cols {
		if c == "namespace" {
			t.Errorf("namespace column still present after migration: %v", cols)
		}
	}
	var nodeCount int
	if err := db.QueryRow(`SELECT count(*) FROM nodes`).Scan(&nodeCount); err != nil {
		t.Fatalf("count nodes: %v", err)
	}
	if nodeCount != 1 {
		t.Errorf("node count = %d, want 1 (data preserved)", nodeCount)
	}
	var auditCount int
	if err := db.QueryRow(`SELECT count(*) FROM audit_log`).Scan(&auditCount); err != nil {
		t.Fatalf("count audit_log: %v", err)
	}
	if auditCount != 1 {
		t.Errorf("audit_log count = %d, want 1 (history preserved)", auditCount)
	}
}

func TestMigratev08tov11_DryRun(t *testing.T) {
	dir := setupV08Dir(t)
	mustWrite(t, filepath.Join(dir, "config.hcl"), []byte("config"))

	if err := Migratev08tov11(MigrateOptions{SourceDir: dir, TargetDir: dir, DryRun: true}); err != nil {
		t.Fatalf("Migratev08tov11 dry-run: %v", err)
	}

	if _, err := os.Stat(filepath.Join(dir, "cluster")); err == nil {
		t.Errorf("dry-run should not create cluster/ dir")
	}
	if _, err := os.Stat(filepath.Join(dir, "orca.db")); err != nil {
		t.Errorf("dry-run should not move orca.db (still at root)")
	}
	if _, err := os.Stat(filepath.Join(dir, "config.hcl")); err != nil {
		t.Errorf("dry-run should not move config.hcl (still at root)")
	}
}

func TestMigratev08tov11_Idempotent(t *testing.T) {
	dir := setupV08Dir(t)
	mustWrite(t, filepath.Join(dir, "config.hcl"), []byte("config"))

	if err := Migratev08tov11(MigrateOptions{SourceDir: dir, TargetDir: dir}); err != nil {
		t.Fatalf("first Migratev08tov11: %v", err)
	}
	if err := Migratev08tov11(MigrateOptions{SourceDir: dir, TargetDir: dir}); err != nil {
		t.Fatalf("second Migratev08tov11: %v", err)
	}

	dbPath := filepath.Join(dir, paths.DefaultNamespace(), "db", "orca.db")
	if _, err := os.Stat(dbPath); err != nil {
		t.Errorf("orca.db missing after second run: %v", err)
	}
}

func TestMigratev08tov11_ImportCA(t *testing.T) {
	dir := setupV08Dir(t)
	mock := &mockCAImporter{}
	caImporterOverride = mock
	t.Cleanup(func() { caImporterOverride = nil })

	if err := Migratev08tov11(MigrateOptions{SourceDir: dir, TargetDir: dir, ImportCA: true}); err != nil {
		t.Fatalf("Migratev08tov11 with ImportCA: %v", err)
	}

	if !mock.called {
		t.Errorf("CA importer was not called")
	}
	wantCert := filepath.Join(dir, "ca.crt")
	if mock.certPath != wantCert {
		t.Errorf("certPath = %q, want %q", mock.certPath, wantCert)
	}
}

func TestMigratev08tov11_ImportCAError(t *testing.T) {
	dir := setupV08Dir(t)
	mock := &mockCAImporter{err: context.Canceled}
	caImporterOverride = mock
	t.Cleanup(func() { caImporterOverride = nil })

	err := Migratev08tov11(MigrateOptions{SourceDir: dir, TargetDir: dir, ImportCA: true})
	if err == nil {
		t.Fatalf("expected error from CA importer")
	}
}

func TestMigratev08tov11_NotV08(t *testing.T) {
	dir := setupV11Dir(t)
	if err := Migratev08tov11(MigrateOptions{SourceDir: dir, TargetDir: dir}); err != nil {
		t.Fatalf("Migratev08tov11 on v0.11 should be no-op, got: %v", err)
	}
}

func TestMigratev08tov11_CrossDir(t *testing.T) {
	src := setupV08Dir(t)
	mustWrite(t, filepath.Join(src, "config.hcl"), []byte("config"))
	tgt := t.TempDir()

	if err := Migratev08tov11(MigrateOptions{SourceDir: src, TargetDir: tgt}); err != nil {
		t.Fatalf("Migratev08tov11 cross-dir: %v", err)
	}

	dbPath := filepath.Join(tgt, paths.DefaultNamespace(), "db", "orca.db")
	if _, err := os.Stat(dbPath); err != nil {
		t.Errorf("orca.db not in target _defaults/db/: %v", err)
	}
	legacyCfg := filepath.Join(tgt, "cluster", "config.legacy.hcl")
	if _, err := os.Stat(legacyCfg); err != nil {
		t.Errorf("config.legacy.hcl not in target cluster/: %v", err)
	}
}
