package sandbox

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

	"go.starlark.net/starlark"
	_ "modernc.org/sqlite"
)

// newTestDB creates an in-memory SQLite database with a test extension table.
func newTestDB(t *testing.T) (*sql.DB, DBModuleConfig) {
	t.Helper()
	db, err := sql.Open("sqlite", ":memory:")
	if err != nil {
		t.Fatalf("open db: %v", err)
	}
	t.Cleanup(func() { db.Close() })

	// Create the extension table the tests will use.
	// physicalTable("logs") for package "test-ext" → ext_test_ext_logs
	_, err = db.Exec(`CREATE TABLE ext_test_ext_logs (
		id         TEXT PRIMARY KEY,
		message    TEXT,
		user_id    TEXT,
		count      INTEGER,
		created_at TEXT DEFAULT (datetime('now'))
	)`)
	if err != nil {
		t.Fatalf("create test table: %v", err)
	}

	cfg := DBModuleConfig{
		PackageID:  "test-ext",
		CanWrite:   true,
		DB:         db,
		IsPostgres: false,
	}
	return db, cfg
}

// execScript runs a Starlark snippet with the db module injected.
func execScript(t *testing.T, cfg DBModuleConfig, script string) (*Result, error) {
	t.Helper()
	sb := New(DefaultConfig())
	ctx := context.Background()
	modules := map[string]starlark.Value{
		"db": BuildDBModule(ctx, cfg),
	}
	return sb.Exec(ctx, "test.star", script, modules)
}

// ── physicalTable ───────────────────────────

func TestPhysicalTable_ValidName(t *testing.T) {
	cfg := DBModuleConfig{PackageID: "my-ext"}
	got, err := cfg.physicalTable("logs")
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if got != "ext_my_ext_logs" {
		t.Errorf("got %q, want ext_my_ext_logs", got)
	}
}

func TestPhysicalTable_EmptyName(t *testing.T) {
	cfg := DBModuleConfig{PackageID: "my-ext"}
	_, err := cfg.physicalTable("")
	if err == nil {
		t.Fatal("expected error for empty table name")
	}
}

func TestPhysicalTable_InvalidChars(t *testing.T) {
	cfg := DBModuleConfig{PackageID: "my-ext"}
	_, err := cfg.physicalTable("bad name")
	if err == nil {
		t.Fatal("expected error for table name with space")
	}
	_, err = cfg.physicalTable("drop;table")
	if err == nil {
		t.Fatal("expected error for table name with semicolon")
	}
}

// ── db.insert / db.query ────────────────────

func TestDBInsertAndQuery(t *testing.T) {
	_, cfg := newTestDB(t)

	result, err := execScript(t, cfg, `
row = db.insert("logs", {"message": "hello", "user_id": "u1"})
rows = db.query("logs")
`)
	if err != nil {
		t.Fatalf("script error: %v", err)
	}

	rowsVal, ok := result.Globals["rows"]
	if !ok {
		t.Fatal("rows not in globals")
	}
	if _, ok := rowsVal.(*starlark.List); !ok {
		t.Fatalf("rows is %T, want *starlark.List", rowsVal)
	}
}

func TestDBQueryFilters(t *testing.T) {
	db, cfg := newTestDB(t)

	// Pre-populate with two rows
	db.Exec(`INSERT INTO ext_test_ext_logs (id, message, user_id) VALUES ('a', 'one', 'u1')`)
	db.Exec(`INSERT INTO ext_test_ext_logs (id, message, user_id) VALUES ('b', 'two', 'u2')`)

	result, err := execScript(t, cfg, `
rows = db.query("logs", filters={"user_id": "u1"})
`)
	if err != nil {
		t.Fatalf("script error: %v", err)
	}

	rowsStr := result.Globals["rows"].String()
	if !strings.Contains(rowsStr, "one") {
		t.Errorf("expected row with message=one, got: %s", rowsStr)
	}
	if strings.Contains(rowsStr, "two") {
		t.Errorf("unexpected row with message=two: %s", rowsStr)
	}
}

func TestDBQueryLimit(t *testing.T) {
	db, cfg := newTestDB(t)
	for i := 0; i < 5; i++ {
		db.Exec(`INSERT INTO ext_test_ext_logs (id, message) VALUES (?, ?)`,
			generateID(), "msg")
	}

	result, err := execScript(t, cfg, `rows = db.query("logs", limit=2)`)
	if err != nil {
		t.Fatalf("script error: %v", err)
	}
	// String representation of a 2-element list has exactly 2 entries
	rowsStr := result.Globals["rows"].String()
	// Count occurrences of "id" key — each row has one
	count := strings.Count(rowsStr, `"id"`)
	if count != 2 {
		t.Errorf("expected 2 rows with limit=2, got repr: %s", rowsStr)
	}
}

// ── db.update ───────────────────────────────

func TestDBUpdate(t *testing.T) {
	db, cfg := newTestDB(t)
	db.Exec(`INSERT INTO ext_test_ext_logs (id, message) VALUES ('r1', 'original')`)

	_, err := execScript(t, cfg, `db.update("logs", "r1", {"message": "updated"})`)
	if err != nil {
		t.Fatalf("script error: %v", err)
	}

	var msg string
	db.QueryRow(`SELECT message FROM ext_test_ext_logs WHERE id = 'r1'`).Scan(&msg)
	if msg != "updated" {
		t.Errorf("expected message=updated, got %q", msg)
	}
}

// ── db.delete ───────────────────────────────

func TestDBDelete(t *testing.T) {
	db, cfg := newTestDB(t)
	db.Exec(`INSERT INTO ext_test_ext_logs (id, message) VALUES ('r1', 'to-delete')`)

	_, err := execScript(t, cfg, `db.delete("logs", "r1")`)
	if err != nil {
		t.Fatalf("script error: %v", err)
	}

	var count int
	db.QueryRow(`SELECT COUNT(*) FROM ext_test_ext_logs WHERE id = 'r1'`).Scan(&count)
	if count != 0 {
		t.Errorf("expected row deleted, count = %d", count)
	}
}

// ── write guard ─────────────────────────────

func TestDBReadOnlyModuleNoWriteFunctions(t *testing.T) {
	_, cfg := newTestDB(t)
	cfg.CanWrite = false

	mod := BuildDBModule(context.Background(), cfg)
	modStr := mod.String()

	// insert/update/delete should not be present in a read-only module
	for _, fn := range []string{"insert", "update", "delete"} {
		if strings.Contains(modStr, fn) {
			t.Errorf("read-only module exposes write function %q", fn)
		}
	}
}

func TestDBWriteFunctionBlockedByReadOnly(t *testing.T) {
	_, cfg := newTestDB(t)
	cfg.CanWrite = false

	_, err := execScript(t, cfg, `db.insert("logs", {"message": "x"})`)
	if err == nil {
		t.Fatal("expected error calling insert on read-only module")
	}
}

// ── view allowlist ───────────────────────────

func TestDBViewRejectsUnknownView(t *testing.T) {
	_, cfg := newTestDB(t)
	_, err := execScript(t, cfg, `db.view("secrets")`)
	if err == nil {
		t.Fatal("expected error for unknown view")
	}
	if !strings.Contains(err.Error(), "unknown view") {
		t.Errorf("unexpected error: %v", err)
	}
}

// ── namespace isolation ──────────────────────

func TestDBQueryRejectsOtherExtensionTable(t *testing.T) {
	_, cfg := newTestDB(t)
	// Attempt to query a table that belongs to a different extension
	_, err := execScript(t, cfg, `db.query("../../etc/passwd")`)
	if err == nil {
		t.Fatal("expected error for path traversal attempt")
	}
}

// ── list_tables ─────────────────────────────

func TestDBListTables(t *testing.T) {
	_, cfg := newTestDB(t)
	result, err := execScript(t, cfg, `names = db.list_tables()`)
	if err != nil {
		t.Fatalf("script error: %v", err)
	}
	namesStr := result.Globals["names"].String()
	// The test table ext_test_ext_logs should appear as "logs"
	if !strings.Contains(namesStr, "logs") {
		t.Errorf("expected 'logs' in list_tables output, got: %s", namesStr)
	}
}

