package localdb

import (
	"path/filepath"
	"testing"
	"time"

	"github.com/glebarez/sqlite"
	"github.com/google/uuid"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
)

func TestRunLocalMigrationsBackfillsExistingConfigurations(t *testing.T) {
	dbPath := filepath.Join(t.TempDir(), "legacy_local.db")

	local, err := New(dbPath)
	if err != nil {
		t.Fatalf("open localdb: %v", err)
	}
	t.Cleanup(func() { _ = local.Close() })

	cfg := &LocalConfiguration{
		UUID:             "legacy-cfg",
		Name:             "Legacy",
		Items:            LocalConfigItems{},
		SyncStatus:       "pending",
		OriginalUsername: "tester",
		IsActive:         true,
	}
	if err := local.SaveConfiguration(cfg); err != nil {
		t.Fatalf("save seed config: %v", err)
	}
	if err := local.DB().Where("configuration_uuid = ?", "legacy-cfg").Delete(&LocalConfigurationVersion{}).Error; err != nil {
		t.Fatalf("delete seed versions: %v", err)
	}
	if err := local.DB().Model(&LocalConfiguration{}).
		Where("uuid = ?", "legacy-cfg").
		Update("current_version_id", nil).Error; err != nil {
		t.Fatalf("clear current_version_id: %v", err)
	}
	if err := local.DB().Where("1=1").Delete(&LocalSchemaMigration{}).Error; err != nil {
		t.Fatalf("clear migration records: %v", err)
	}

	if err := runLocalMigrations(local.DB()); err != nil {
		t.Fatalf("run local migrations manually: %v", err)
	}

	migratedCfg, err := local.GetConfigurationByUUID("legacy-cfg")
	if err != nil {
		t.Fatalf("get migrated config: %v", err)
	}
	if migratedCfg.CurrentVersionID == nil || *migratedCfg.CurrentVersionID == "" {
		t.Fatalf("expected current_version_id after migration")
	}
	if !migratedCfg.IsActive {
		t.Fatalf("expected migrated config to be active")
	}

	var versionCount int64
	if err := local.DB().Model(&LocalConfigurationVersion{}).
		Where("configuration_uuid = ?", "legacy-cfg").
		Count(&versionCount).Error; err != nil {
		t.Fatalf("count versions: %v", err)
	}
	if versionCount != 1 {
		t.Fatalf("expected 1 backfilled version, got %d", versionCount)
	}

	var migrationCount int64
	if err := local.DB().Model(&LocalSchemaMigration{}).Count(&migrationCount).Error; err != nil {
		t.Fatalf("count local migrations: %v", err)
	}
	if migrationCount == 0 {
		t.Fatalf("expected local migrations to be recorded")
	}
}

func TestRunLocalMigrationsFixesPricelistVersionUniqueIndex(t *testing.T) {
	dbPath := filepath.Join(t.TempDir(), "pricelist_index_fix.db")

	local, err := New(dbPath)
	if err != nil {
		t.Fatalf("open localdb: %v", err)
	}
	t.Cleanup(func() { _ = local.Close() })

	if err := local.SaveLocalPricelist(&LocalPricelist{
		ServerID:  10,
		Version:   "2026-02-06-001",
		Name:      "v1",
		CreatedAt: time.Now().Add(-time.Hour),
		SyncedAt:  time.Now().Add(-time.Hour),
	}); err != nil {
		t.Fatalf("save first pricelist: %v", err)
	}

	if err := local.DB().Exec(`
		CREATE UNIQUE INDEX IF NOT EXISTS idx_local_pricelists_version_legacy
		ON local_pricelists(version)
	`).Error; err != nil {
		t.Fatalf("create legacy unique version index: %v", err)
	}

	if err := local.DB().Where("id = ?", "2026_02_06_pricelist_index_fix").
		Delete(&LocalSchemaMigration{}).Error; err != nil {
		t.Fatalf("delete migration record: %v", err)
	}

	if err := runLocalMigrations(local.DB()); err != nil {
		t.Fatalf("rerun local migrations: %v", err)
	}

	if err := local.SaveLocalPricelist(&LocalPricelist{
		ServerID:  11,
		Version:   "2026-02-06-001",
		Name:      "v1-duplicate-version",
		CreatedAt: time.Now(),
		SyncedAt:  time.Now(),
	}); err != nil {
		t.Fatalf("save second pricelist with duplicate version: %v", err)
	}

	var count int64
	if err := local.DB().Model(&LocalPricelist{}).Count(&count).Error; err != nil {
		t.Fatalf("count pricelists: %v", err)
	}
	if count != 2 {
		t.Fatalf("expected 2 pricelists, got %d", count)
	}
}

func TestRunLocalMigrationsDeduplicatesConfigurationVersionsBySpecAndPrice(t *testing.T) {
	dbPath := filepath.Join(t.TempDir(), "versions_dedup.db")

	local, err := New(dbPath)
	if err != nil {
		t.Fatalf("open localdb: %v", err)
	}
	t.Cleanup(func() { _ = local.Close() })

	cfg := &LocalConfiguration{
		UUID:             "dedup-cfg",
		Name:             "Dedup",
		Items:            LocalConfigItems{{LotName: "CPU_A", Quantity: 1, UnitPrice: 100}},
		ServerCount:      1,
		SyncStatus:       "pending",
		OriginalUsername: "tester",
		IsActive:         true,
	}
	if err := local.SaveConfiguration(cfg); err != nil {
		t.Fatalf("save seed config: %v", err)
	}

	baseV1Data, err := BuildConfigurationSnapshot(cfg)
	if err != nil {
		t.Fatalf("build v1 snapshot: %v", err)
	}
	baseV1 := LocalConfigurationVersion{
		ID:                uuid.NewString(),
		ConfigurationUUID: cfg.UUID,
		VersionNo:         1,
		Data:              baseV1Data,
		AppVersion:        "test",
		CreatedAt:         time.Now(),
	}
	if err := local.DB().Create(&baseV1).Error; err != nil {
		t.Fatalf("insert base v1: %v", err)
	}
	if err := local.DB().Model(&LocalConfiguration{}).
		Where("uuid = ?", cfg.UUID).
		Update("current_version_id", baseV1.ID).Error; err != nil {
		t.Fatalf("set current_version_id to v1: %v", err)
	}

	v2 := LocalConfigurationVersion{
		ID:                uuid.NewString(),
		ConfigurationUUID: cfg.UUID,
		VersionNo:         2,
		Data:              baseV1.Data,
		AppVersion:        "test",
		CreatedAt:         time.Now().Add(1 * time.Second),
	}
	if err := local.DB().Create(&v2).Error; err != nil {
		t.Fatalf("insert duplicate v2: %v", err)
	}

	modified := *cfg
	modified.Items = LocalConfigItems{{LotName: "CPU_A", Quantity: 2, UnitPrice: 100}}
	total := modified.Items.Total()
	modified.TotalPrice = &total
	modified.UpdatedAt = time.Now()
	v3Data, err := BuildConfigurationSnapshot(&modified)
	if err != nil {
		t.Fatalf("build v3 snapshot: %v", err)
	}

	v3 := LocalConfigurationVersion{
		ID:                uuid.NewString(),
		ConfigurationUUID: cfg.UUID,
		VersionNo:         3,
		Data:              v3Data,
		AppVersion:        "test",
		CreatedAt:         time.Now().Add(2 * time.Second),
	}
	if err := local.DB().Create(&v3).Error; err != nil {
		t.Fatalf("insert v3: %v", err)
	}

	v4 := LocalConfigurationVersion{
		ID:                uuid.NewString(),
		ConfigurationUUID: cfg.UUID,
		VersionNo:         4,
		Data:              v3Data,
		AppVersion:        "test",
		CreatedAt:         time.Now().Add(3 * time.Second),
	}
	if err := local.DB().Create(&v4).Error; err != nil {
		t.Fatalf("insert duplicate v4: %v", err)
	}

	if err := local.DB().Model(&LocalConfiguration{}).
		Where("uuid = ?", cfg.UUID).
		Update("current_version_id", v4.ID).Error; err != nil {
		t.Fatalf("point current_version_id to duplicate v4: %v", err)
	}

	if err := local.DB().Where("id = ?", "2026_02_19_configuration_versions_dedup_spec_price").
		Delete(&LocalSchemaMigration{}).Error; err != nil {
		t.Fatalf("delete dedup migration record: %v", err)
	}

	if err := runLocalMigrations(local.DB()); err != nil {
		t.Fatalf("rerun local migrations: %v", err)
	}

	var versions []LocalConfigurationVersion
	if err := local.DB().Where("configuration_uuid = ?", cfg.UUID).
		Order("version_no ASC").
		Find(&versions).Error; err != nil {
		t.Fatalf("load versions after dedup: %v", err)
	}
	if len(versions) != 2 {
		t.Fatalf("expected 2 versions after dedup, got %d", len(versions))
	}
	if versions[0].VersionNo != 1 || versions[1].VersionNo != 3 {
		t.Fatalf("expected kept version numbers [1,3], got [%d,%d]", versions[0].VersionNo, versions[1].VersionNo)
	}

	var after LocalConfiguration
	if err := local.DB().Where("uuid = ?", cfg.UUID).First(&after).Error; err != nil {
		t.Fatalf("load config after dedup: %v", err)
	}
	if after.CurrentVersionID == nil || *after.CurrentVersionID != v3.ID {
		t.Fatalf("expected current_version_id to point to kept latest version v3")
	}
}

func TestRunLocalMigrationsBackfillsConfigurationLineNo(t *testing.T) {
	dbPath := filepath.Join(t.TempDir(), "line_no_backfill.db")

	local, err := New(dbPath)
	if err != nil {
		t.Fatalf("open localdb: %v", err)
	}
	t.Cleanup(func() { _ = local.Close() })

	projectUUID := "project-line"
	cfg1 := &LocalConfiguration{
		UUID:             "line-cfg-1",
		ProjectUUID:      &projectUUID,
		Name:             "Cfg 1",
		Items:            LocalConfigItems{},
		SyncStatus:       "pending",
		OriginalUsername: "tester",
		IsActive:         true,
		CreatedAt:        time.Now().Add(-2 * time.Hour),
	}
	cfg2 := &LocalConfiguration{
		UUID:             "line-cfg-2",
		ProjectUUID:      &projectUUID,
		Name:             "Cfg 2",
		Items:            LocalConfigItems{},
		SyncStatus:       "pending",
		OriginalUsername: "tester",
		IsActive:         true,
		CreatedAt:        time.Now().Add(-1 * time.Hour),
	}
	if err := local.SaveConfiguration(cfg1); err != nil {
		t.Fatalf("save cfg1: %v", err)
	}
	if err := local.SaveConfiguration(cfg2); err != nil {
		t.Fatalf("save cfg2: %v", err)
	}

	if err := local.DB().Model(&LocalConfiguration{}).Where("uuid IN ?", []string{cfg1.UUID, cfg2.UUID}).Update("line_no", 0).Error; err != nil {
		t.Fatalf("reset line_no: %v", err)
	}
	if err := local.DB().Where("id = ?", "2026_02_19_local_config_line_no").Delete(&LocalSchemaMigration{}).Error; err != nil {
		t.Fatalf("delete migration record: %v", err)
	}

	if err := runLocalMigrations(local.DB()); err != nil {
		t.Fatalf("rerun local migrations: %v", err)
	}

	var rows []LocalConfiguration
	if err := local.DB().Where("uuid IN ?", []string{cfg1.UUID, cfg2.UUID}).Order("created_at ASC").Find(&rows).Error; err != nil {
		t.Fatalf("load configurations: %v", err)
	}
	if len(rows) != 2 {
		t.Fatalf("expected 2 configurations, got %d", len(rows))
	}
	if rows[0].Line != 10 || rows[1].Line != 20 {
		t.Fatalf("expected line_no [10,20], got [%d,%d]", rows[0].Line, rows[1].Line)
	}
}

func TestRunLocalMigrationsDeduplicatesCanonicalPartnumberCatalog(t *testing.T) {
	dbPath := filepath.Join(t.TempDir(), "partnumber_catalog_dedup.db")
	db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{
		Logger: logger.Default.LogMode(logger.Silent),
	})
	if err != nil {
		t.Fatalf("open sqlite: %v", err)
	}

	firstLots := LocalPartnumberBookLots{
		{LotName: "LOT-A", Qty: 1},
	}
	secondLots := LocalPartnumberBookLots{
		{LotName: "LOT-B", Qty: 2},
	}

	if err := db.Exec(`
		CREATE TABLE local_partnumber_book_items (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			partnumber TEXT NOT NULL,
			lots_json TEXT NOT NULL,
			description TEXT
		)
	`).Error; err != nil {
		t.Fatalf("create dirty local_partnumber_book_items: %v", err)
	}

	if err := db.Create(&LocalPartnumberBookItem{
		Partnumber:  "PN-001",
		LotsJSON:    firstLots,
		Description: "",
	}).Error; err != nil {
		t.Fatalf("insert first duplicate row: %v", err)
	}
	if err := db.Create(&LocalPartnumberBookItem{
		Partnumber:  "PN-001",
		LotsJSON:    secondLots,
		Description: "Canonical description",
	}).Error; err != nil {
		t.Fatalf("insert second duplicate row: %v", err)
	}

	if err := migrateLocalPartnumberBookCatalog(db); err != nil {
		t.Fatalf("migrate local partnumber catalog: %v", err)
	}

	var items []LocalPartnumberBookItem
	if err := db.Order("partnumber ASC").Find(&items).Error; err != nil {
		t.Fatalf("load migrated partnumber items: %v", err)
	}
	if len(items) != 1 {
		t.Fatalf("expected 1 deduplicated item, got %d", len(items))
	}
	if items[0].Partnumber != "PN-001" {
		t.Fatalf("unexpected partnumber: %s", items[0].Partnumber)
	}
	if items[0].Description != "Canonical description" {
		t.Fatalf("expected merged description, got %q", items[0].Description)
	}
	if len(items[0].LotsJSON) != 2 {
		t.Fatalf("expected merged lots from duplicates, got %d", len(items[0].LotsJSON))
	}

	var duplicateCount int64
	if err := db.Model(&LocalPartnumberBookItem{}).
		Where("partnumber = ?", "PN-001").
		Count(&duplicateCount).Error; err != nil {
		t.Fatalf("count deduplicated partnumber: %v", err)
	}
	if duplicateCount != 1 {
		t.Fatalf("expected unique partnumber row after migration, got %d", duplicateCount)
	}
}

func TestSanitizeLocalPartnumberBookCatalogRemovesRowsWithoutPartnumber(t *testing.T) {
	dbPath := filepath.Join(t.TempDir(), "sanitize_partnumber_catalog.db")
	db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{
		Logger: logger.Default.LogMode(logger.Silent),
	})
	if err != nil {
		t.Fatalf("open sqlite: %v", err)
	}

	if err := db.Exec(`
		CREATE TABLE local_partnumber_book_items (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			partnumber TEXT NULL,
			lots_json TEXT NOT NULL,
			description TEXT
		)
	`).Error; err != nil {
		t.Fatalf("create local_partnumber_book_items: %v", err)
	}
	if err := db.Exec(`
		INSERT INTO local_partnumber_book_items (partnumber, lots_json, description) VALUES
		(NULL, '[]', 'null pn'),
		('', '[]', 'empty pn'),
		('PN-OK', '[]', 'valid pn')
	`).Error; err != nil {
		t.Fatalf("seed local_partnumber_book_items: %v", err)
	}

	if err := sanitizeLocalPartnumberBookCatalog(db); err != nil {
		t.Fatalf("sanitize local partnumber catalog: %v", err)
	}

	var items []LocalPartnumberBookItem
	if err := db.Order("id ASC").Find(&items).Error; err != nil {
		t.Fatalf("load sanitized items: %v", err)
	}
	if len(items) != 1 {
		t.Fatalf("expected 1 valid item after sanitize, got %d", len(items))
	}
	if items[0].Partnumber != "PN-OK" {
		t.Fatalf("expected remaining partnumber PN-OK, got %q", items[0].Partnumber)
	}
}

func TestNewMigratesLegacyPartnumberBookCatalogBeforeAutoMigrate(t *testing.T) {
	dbPath := filepath.Join(t.TempDir(), "legacy_partnumber_catalog.db")
	db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{
		Logger: logger.Default.LogMode(logger.Silent),
	})
	if err != nil {
		t.Fatalf("open sqlite: %v", err)
	}

	if err := db.Exec(`
		CREATE TABLE local_partnumber_book_items (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			partnumber TEXT NOT NULL UNIQUE,
			lots_json TEXT NOT NULL,
			is_primary_pn INTEGER NOT NULL DEFAULT 0,
			description TEXT
		)
	`).Error; err != nil {
		t.Fatalf("create legacy local_partnumber_book_items: %v", err)
	}
	if err := db.Exec(`
		INSERT INTO local_partnumber_book_items (partnumber, lots_json, is_primary_pn, description)
		VALUES ('PN-001', '[{"lot_name":"CPU_A","qty":1}]', 0, 'Legacy row')
	`).Error; err != nil {
		t.Fatalf("seed legacy local_partnumber_book_items: %v", err)
	}

	local, err := New(dbPath)
	if err != nil {
		t.Fatalf("open localdb with legacy catalog: %v", err)
	}
	t.Cleanup(func() { _ = local.Close() })

	var columns []struct {
		Name string `gorm:"column:name"`
	}
	if err := local.DB().Raw(`SELECT name FROM pragma_table_info('local_partnumber_book_items')`).Scan(&columns).Error; err != nil {
		t.Fatalf("load local_partnumber_book_items columns: %v", err)
	}
	for _, column := range columns {
		if column.Name == "is_primary_pn" {
			t.Fatalf("expected legacy is_primary_pn column to be removed before automigrate")
		}
	}

	var items []LocalPartnumberBookItem
	if err := local.DB().Find(&items).Error; err != nil {
		t.Fatalf("load migrated local_partnumber_book_items: %v", err)
	}
	if len(items) != 1 || items[0].Partnumber != "PN-001" {
		t.Fatalf("unexpected migrated rows: %#v", items)
	}
}

func TestNewRecoversBrokenPartnumberBookCatalogCache(t *testing.T) {
	dbPath := filepath.Join(t.TempDir(), "broken_partnumber_catalog.db")
	db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{
		Logger: logger.Default.LogMode(logger.Silent),
	})
	if err != nil {
		t.Fatalf("open sqlite: %v", err)
	}

	if err := db.Exec(`
		CREATE TABLE local_partnumber_book_items (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			partnumber TEXT NOT NULL UNIQUE,
			lots_json TEXT NOT NULL,
			description TEXT
		)
	`).Error; err != nil {
		t.Fatalf("create broken local_partnumber_book_items: %v", err)
	}
	if err := db.Exec(`
		INSERT INTO local_partnumber_book_items (partnumber, lots_json, description)
		VALUES ('PN-001', '{not-json}', 'Broken cache row')
	`).Error; err != nil {
		t.Fatalf("seed broken local_partnumber_book_items: %v", err)
	}

	local, err := New(dbPath)
	if err != nil {
		t.Fatalf("open localdb with broken catalog cache: %v", err)
	}
	t.Cleanup(func() { _ = local.Close() })

	var count int64
	if err := local.DB().Model(&LocalPartnumberBookItem{}).Count(&count).Error; err != nil {
		t.Fatalf("count recovered local_partnumber_book_items: %v", err)
	}
	if count != 0 {
		t.Fatalf("expected empty recovered local_partnumber_book_items, got %d rows", count)
	}

	var quarantineTables []struct {
		Name string `gorm:"column:name"`
	}
	if err := local.DB().Raw(`
		SELECT name
		FROM sqlite_master
		WHERE type = 'table' AND name LIKE 'local_partnumber_book_items_broken_%'
	`).Scan(&quarantineTables).Error; err != nil {
		t.Fatalf("load quarantine tables: %v", err)
	}
	if len(quarantineTables) != 1 {
		t.Fatalf("expected one quarantined broken catalog table, got %d", len(quarantineTables))
	}
}

func TestCleanupStaleReadOnlyCacheTempTablesDropsShadowTempWhenBaseExists(t *testing.T) {
	dbPath := filepath.Join(t.TempDir(), "stale_cache_temp.db")
	db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{
		Logger: logger.Default.LogMode(logger.Silent),
	})
	if err != nil {
		t.Fatalf("open sqlite: %v", err)
	}

	if err := db.Exec(`
		CREATE TABLE local_pricelist_items (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			pricelist_id INTEGER NOT NULL,
			partnumber TEXT,
			brand TEXT NOT NULL DEFAULT '',
			lot_name TEXT NOT NULL,
			description TEXT,
			price REAL NOT NULL DEFAULT 0,
			quantity INTEGER NOT NULL DEFAULT 0,
			reserve INTEGER NOT NULL DEFAULT 0,
			available_qty REAL,
			partnumbers TEXT,
			lot_category TEXT,
			created_at DATETIME,
			updated_at DATETIME
		)
	`).Error; err != nil {
		t.Fatalf("create local_pricelist_items: %v", err)
	}
	if err := db.Exec(`
		CREATE TABLE local_pricelist_items__temp (
			id INTEGER PRIMARY KEY AUTOINCREMENT,
			legacy TEXT
		)
	`).Error; err != nil {
		t.Fatalf("create local_pricelist_items__temp: %v", err)
	}

	if err := cleanupStaleReadOnlyCacheTempTables(db); err != nil {
		t.Fatalf("cleanup stale read-only cache temp tables: %v", err)
	}

	if db.Migrator().HasTable("local_pricelist_items__temp") {
		t.Fatalf("expected stale temp table to be dropped")
	}
	if !db.Migrator().HasTable("local_pricelist_items") {
		t.Fatalf("expected base local_pricelist_items table to remain")
	}
}
