package service

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

	"github.com/libtnb/sqlite"
	"github.com/stretchr/testify/require"
	"gorm.io/gorm"

	"github.com/pocket-id/pocket-id/backend/internal/common"
	"github.com/pocket-id/pocket-id/backend/internal/utils"
)

// importResetTargetVersion is a real migration version the import resets the schema to
// It only has to be at or beyond 20240817191051 (the rename_config_table migration that toggles foreign keys), so the reset exercises a foreign-key-sensitive migration
const importResetTargetVersion = 20260707170000

// openImportTestDB opens a Gorm SQLite pool on a file database using the same pragmas the application configures in production, most importantly foreign_keys(1) on every connection (normalize() is already registered by the service package's test setup)
func openImportTestDB(t *testing.T, dbPath string, cfg func(*sql.DB)) *gorm.DB {
	t.Helper()
	common.EnvConfig.DbProvider = common.DbProviderSqlite

	dsn := "file:" + dbPath + "?_txlock=immediate&_pragma=busy_timeout(2500)&_pragma=journal_mode(WAL)&_pragma=foreign_keys(1)"

	db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{TranslateError: true})
	require.NoError(t, err)

	if cfg != nil {
		sqlDB, _ := db.DB()
		cfg(sqlDB)
	}

	return db
}

func closeImportTestDB(db *gorm.DB) {
	sqlDB, err := db.DB()
	if err == nil {
		_ = sqlDB.Close()
	}
}

// seedActorHostSchema simulates the actor host's own tables and views (the "francis_" prefix), including a view over a table, which an import must preserve.
func seedActorHostSchema(t *testing.T, db *gorm.DB) {
	t.Helper()
	require.NoError(t, db.Exec(`CREATE TABLE francis_active_actors (id TEXT PRIMARY KEY)`).Error)
	require.NoError(t, db.Exec(`CREATE VIEW francis_host_active_actor_count AS SELECT count(*) AS n FROM francis_active_actors`).Error)
	require.NoError(t, db.Exec(`INSERT INTO francis_active_actors (id) VALUES ('actor-1')`).Error)
}

// requireActorHostSchemaPreserved asserts the actor host's tables/views and their data survived an import.
func requireActorHostSchemaPreserved(t *testing.T, db *gorm.DB) {
	t.Helper()
	var tableRows int64
	err := db.Raw(`SELECT count(*) FROM francis_active_actors`).Scan(&tableRows).Error
	require.NoError(t, err)
	require.Equal(t, int64(1), tableRows, "francis_ tables and their rows must be preserved by an import")

	// The view is only valid if its backing table was preserved as well
	var viewCount int64
	err = db.Raw(`SELECT n FROM francis_host_active_actor_count`).Scan(&viewCount).Error
	require.NoError(t, err)
	require.Equal(t, int64(1), viewCount, "francis_ views must be preserved by an import")
}

// importAndRestart mirrors the CLI import flow (tests/specs/cli.spec.ts): a running instance is stopped, `pocket-id import` replaces the schema and data, and the instance is started again.
// Each step uses a fresh pool to emulate the separate processes involved. importCfg configures the pool the import step uses.
func importAndRestart(importCfg func(*sql.DB)) func(t *testing.T) {
	return func(t *testing.T) {
		dbPath := filepath.Join(t.TempDir(), "pocket-id.db")

		// Initial boot: connect, migrate, and let the actor host create its own schema.
		boot := openImportTestDB(t, dbPath, nil)
		sqlDB, _ := boot.DB()
		err := utils.MigrateDatabase(t.Context(), sqlDB)
		require.NoError(t, err)
		seedActorHostSchema(t, boot)
		closeImportTestDB(boot)

		// Import: reset the schema (drop + migrate) and load data. Empty tables keep the test focused
		// on the schema reset.
		imp := openImportTestDB(t, dbPath, importCfg)
		err = NewImportService(imp, nil, nil).ImportDatabase(t.Context(), DatabaseExport{
			Provider: "sqlite",
			Version:  importResetTargetVersion,
			Tables:   map[string][]map[string]any{},
		})
		closeImportTestDB(imp)
		require.NoError(t, err)

		// Restart: migrating again must be a clean no-op, not fail on a dirty database, and the actor
		// host's own schema must still be intact
		restart := openImportTestDB(t, dbPath, nil)
		defer closeImportTestDB(restart)
		sqlDB, _ = restart.DB()
		err = utils.MigrateDatabase(t.Context(), sqlDB)
		require.NoError(t, err)
		requireActorHostSchemaPreserved(t, restart)
	}
}

func TestImportResetSchema(t *testing.T) {
	importAndRestart(nil)(t)
}

// TestImportResetSchemaFreshConnections guards the schema reset against connection pooling.
// resetSchema must drop tables with foreign keys disabled on the connection that performs the drops.
// With MaxIdleConns(0) every statement runs on a fresh connection (which the DSN opens with foreign_keys(1))
// Before dropPocketIDTablesSQLite this reproduced the CI failure where DROP TABLE tripped foreign-key cascades/triggers and left the database dirty, breaking the container restart.
func TestImportResetSchemaFreshConnections(t *testing.T) {
	importAndRestart(func(db *sql.DB) { db.SetMaxIdleConns(0) })(t)
}
