package handlers

import (
	"context"
	"database/sql"
	"strings"
	"testing"

	_ "modernc.org/sqlite"

	"switchboard-core/store"
)

// ── mock ExtDataStore ────────────────────────────────────────────────────────

// memExtDataStore is an in-memory ExtDataStore for testing.
type memExtDataStore struct {
	tables map[string][]string // packageID → []logicalName
}

func newMemExtDataStore() *memExtDataStore {
	return &memExtDataStore{tables: map[string][]string{}}
}

func (m *memExtDataStore) RegisterTable(_ context.Context, packageID, tableName string) error {
	m.tables[packageID] = append(m.tables[packageID], tableName)
	return nil
}

func (m *memExtDataStore) ListTables(_ context.Context, packageID string) ([]string, error) {
	return m.tables[packageID], nil
}

func (m *memExtDataStore) DeletePackageTables(_ context.Context, packageID string) error {
	delete(m.tables, packageID)
	return nil
}

// ── helpers ──────────────────────────────────────────────────────────────────

func newSchemaTestDB(t *testing.T) *sql.DB {
	t.Helper()
	db, err := sql.Open("sqlite", ":memory:")
	if err != nil {
		t.Fatalf("open db: %v", err)
	}
	t.Cleanup(func() { db.Close() })
	return db
}

func storesWithExtData(m *memExtDataStore) store.Stores {
	return store.Stores{ExtData: m}
}

// ── ParseDBTables ────────────────────────────────────────────────────────────

func TestParseDBTables_Valid(t *testing.T) {
	manifest := map[string]any{
		"db_tables": map[string]any{
			"logs": map[string]any{
				"columns": map[string]any{
					"message": "text",
					"count":   "int",
				},
				"indexes": []any{[]any{"message"}},
			},
		},
	}
	tables, ok := ParseDBTables(manifest)
	if !ok {
		t.Fatal("expected ok=true")
	}
	if _, has := tables["logs"]; !has {
		t.Fatal("expected 'logs' in parsed tables")
	}
	if tables["logs"].Columns["message"] != "text" {
		t.Errorf("expected column message=text, got %q", tables["logs"].Columns["message"])
	}
	if len(tables["logs"].Indexes) != 1 {
		t.Errorf("expected 1 index, got %d", len(tables["logs"].Indexes))
	}
}

func TestParseDBTables_Missing(t *testing.T) {
	_, ok := ParseDBTables(map[string]any{"title": "hi"})
	if ok {
		t.Fatal("expected ok=false when db_tables absent")
	}
}

func TestParseDBTables_InvalidTableName(t *testing.T) {
	manifest := map[string]any{
		"db_tables": map[string]any{
			"bad name":  map[string]any{},
			"good_name": map[string]any{"columns": map[string]any{"x": "text"}},
		},
	}
	tables, ok := ParseDBTables(manifest)
	if !ok {
		t.Fatal("expected ok=true (good_name is valid)")
	}
	if _, has := tables["bad name"]; has {
		t.Error("invalid table name should be skipped")
	}
	if _, has := tables["good_name"]; !has {
		t.Error("valid table name should be present")
	}
}

func TestParseDBTables_InvalidColumnName(t *testing.T) {
	manifest := map[string]any{
		"db_tables": map[string]any{
			"events": map[string]any{
				"columns": map[string]any{
					"bad col": "text",
					"ok_col":  "text",
				},
			},
		},
	}
	tables, ok := ParseDBTables(manifest)
	if !ok {
		t.Fatal("expected ok=true")
	}
	if _, has := tables["events"].Columns["bad col"]; has {
		t.Error("invalid column name should be skipped")
	}
	if _, has := tables["events"].Columns["ok_col"]; !has {
		t.Error("valid column name should be present")
	}
}

// ── mapColType ───────────────────────────────────────────────────────────────

func TestMapColType_SQLite(t *testing.T) {
	cases := []struct{ in, want string }{
		{"text", "TEXT"},
		{"int", "INTEGER"},
		{"integer", "INTEGER"},
		{"real", "REAL"},
		{"float", "REAL"},
		{"bool", "INTEGER"},
		{"boolean", "INTEGER"},
		{"timestamp", "TEXT"},
		{"unknown", "TEXT"},
	}
	for _, c := range cases {
		got := mapColType(c.in, false)
		if got != c.want {
			t.Errorf("mapColType(%q, sqlite) = %q, want %q", c.in, got, c.want)
		}
	}
}

func TestMapColType_Postgres(t *testing.T) {
	if mapColType("bool", true) != "BOOLEAN" {
		t.Error("expected BOOLEAN for bool in postgres")
	}
	if mapColType("timestamp", true) != "TIMESTAMPTZ" {
		t.Error("expected TIMESTAMPTZ for timestamp in postgres")
	}
	if mapColType("int", true) != "INTEGER" {
		t.Error("expected INTEGER for int in postgres")
	}
}

// ── extPhysicalTable ─────────────────────────────────────────────────────────

func TestExtPhysicalTable(t *testing.T) {
	got := extPhysicalTable("my-ext", "logs")
	if got != "ext_my_ext_logs" {
		t.Errorf("got %q, want ext_my_ext_logs", got)
	}
	got = extPhysicalTable("simple", "events")
	if got != "ext_simple_events" {
		t.Errorf("got %q, want ext_simple_events", got)
	}
}

// ── CreateExtTables ──────────────────────────────────────────────────────────

func TestCreateExtTables_CreatesTable(t *testing.T) {
	db := newSchemaTestDB(t)
	m := newMemExtDataStore()
	ctx := context.Background()

	tables := map[string]TableDef{
		"logs": {
			Columns: map[string]string{"message": "text", "count": "int"},
		},
	}
	if err := CreateExtTables(ctx, db, false, storesWithExtData(m), "test-pkg", tables); err != nil {
		t.Fatalf("CreateExtTables: %v", err)
	}

	// Verify table exists by inserting a row.
	_, err := db.ExecContext(ctx,
		`INSERT INTO ext_test_pkg_logs (id, message, count) VALUES ('1', 'hello', 42)`)
	if err != nil {
		t.Errorf("table not created or wrong schema: %v", err)
	}
}

func TestCreateExtTables_WithIndexes(t *testing.T) {
	db := newSchemaTestDB(t)
	m := newMemExtDataStore()
	ctx := context.Background()

	tables := map[string]TableDef{
		"events": {
			Columns: map[string]string{"user_id": "text", "kind": "text"},
			Indexes: [][]string{{"user_id"}, {"user_id", "kind"}},
		},
	}
	if err := CreateExtTables(ctx, db, false, storesWithExtData(m), "idx-pkg", tables); err != nil {
		t.Fatalf("CreateExtTables: %v", err)
	}

	// Verify indexes exist via sqlite_master.
	rows, err := db.QueryContext(ctx,
		`SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='ext_idx_pkg_events'`)
	if err != nil {
		t.Fatalf("query indexes: %v", err)
	}
	defer rows.Close()
	var idxNames []string
	for rows.Next() {
		var name string
		rows.Scan(&name)
		idxNames = append(idxNames, name)
	}
	if len(idxNames) < 2 {
		t.Errorf("expected at least 2 indexes, got %d: %v", len(idxNames), idxNames)
	}
}

func TestCreateExtTables_CatalogRegistration(t *testing.T) {
	db := newSchemaTestDB(t)
	m := newMemExtDataStore()
	ctx := context.Background()

	tables := map[string]TableDef{
		"logs":   {Columns: map[string]string{"msg": "text"}},
		"errors": {Columns: map[string]string{"detail": "text"}},
	}
	if err := CreateExtTables(ctx, db, false, storesWithExtData(m), "cat-pkg", tables); err != nil {
		t.Fatalf("CreateExtTables: %v", err)
	}

	registered := m.tables["cat-pkg"]
	if len(registered) != 2 {
		t.Errorf("expected 2 registered tables, got %d: %v", len(registered), registered)
	}
}

func TestCreateExtTables_NilDBIsNoop(t *testing.T) {
	m := newMemExtDataStore()
	ctx := context.Background()
	// Should not panic or error.
	err := CreateExtTables(ctx, nil, false, storesWithExtData(m), "pkg", map[string]TableDef{
		"t": {Columns: map[string]string{"x": "text"}},
	})
	if err != nil {
		t.Errorf("expected nil error for nil db, got: %v", err)
	}
}

// ── DropExtTables ─────────────────────────────────────────────────────────────

func TestDropExtTables_DropsAndClearsCatalog(t *testing.T) {
	db := newSchemaTestDB(t)
	m := newMemExtDataStore()
	ctx := context.Background()

	// Create table first.
	tables := map[string]TableDef{
		"items": {Columns: map[string]string{"name": "text"}},
	}
	if err := CreateExtTables(ctx, db, false, storesWithExtData(m), "drop-pkg", tables); err != nil {
		t.Fatalf("CreateExtTables: %v", err)
	}

	// Drop.
	if err := DropExtTables(ctx, db, storesWithExtData(m), "drop-pkg"); err != nil {
		t.Fatalf("DropExtTables: %v", err)
	}

	// Table should be gone.
	var count int
	db.QueryRowContext(ctx,
		`SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='ext_drop_pkg_items'`,
	).Scan(&count)
	if count != 0 {
		t.Error("expected table to be dropped, still present")
	}

	// Catalog should be empty.
	if len(m.tables["drop-pkg"]) != 0 {
		t.Errorf("expected empty catalog, got: %v", m.tables["drop-pkg"])
	}
}

func TestDropExtTables_MultipleTables(t *testing.T) {
	db := newSchemaTestDB(t)
	m := newMemExtDataStore()
	ctx := context.Background()

	tables := map[string]TableDef{
		"alpha": {Columns: map[string]string{"v": "text"}},
		"beta":  {Columns: map[string]string{"v": "text"}},
	}
	CreateExtTables(ctx, db, false, storesWithExtData(m), "multi-pkg", tables)
	DropExtTables(ctx, db, storesWithExtData(m), "multi-pkg")

	for _, name := range []string{"ext_multi_pkg_alpha", "ext_multi_pkg_beta"} {
		var count int
		db.QueryRowContext(ctx,
			`SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?`, name,
		).Scan(&count)
		if count != 0 {
			t.Errorf("expected %s to be dropped", name)
		}
	}
}

func TestCreateExtTables_DDLContainsAutoColumns(t *testing.T) {
	// Verify that the DDL-generated table has 'id' and 'created_at' columns
	// even when no columns are declared in the manifest.
	db := newSchemaTestDB(t)
	m := newMemExtDataStore()
	ctx := context.Background()

	tables := map[string]TableDef{"empty": {Columns: map[string]string{}}}
	if err := CreateExtTables(ctx, db, false, storesWithExtData(m), "auto-pkg", tables); err != nil {
		t.Fatalf("CreateExtTables: %v", err)
	}

	// Insert with just id — should work (created_at has default).
	_, err := db.ExecContext(ctx, `INSERT INTO ext_auto_pkg_empty (id) VALUES ('x')`)
	if err != nil {
		t.Errorf("expected auto-columns (id, created_at) to exist: %v", err)
	}

	// Verify created_at was populated.
	var ca string
	db.QueryRowContext(ctx, `SELECT created_at FROM ext_auto_pkg_empty WHERE id='x'`).Scan(&ca)
	if strings.TrimSpace(ca) == "" {
		t.Error("expected created_at to be populated by default")
	}
}
