package sync_test

import (
	"encoding/json"
	"fmt"
	"path/filepath"
	"testing"
	"time"

	"git.mchus.pro/mchus/quoteforge/internal/localdb"
	"git.mchus.pro/mchus/quoteforge/internal/models"
	"git.mchus.pro/mchus/quoteforge/internal/services"
	syncsvc "git.mchus.pro/mchus/quoteforge/internal/services/sync"
	"github.com/glebarez/sqlite"
	"gorm.io/gorm"
)

func TestPushPendingChangesProjectsBeforeConfigurations(t *testing.T) {
	local := newLocalDBForSyncTest(t)
	serverDB := newServerDBForSyncTest(t)

	localSync := syncsvc.NewService(nil, local)
	projectService := services.NewProjectService(local)
	configService := services.NewLocalConfigurationService(local, localSync, &services.QuoteService{}, func() bool { return false })

	project, err := projectService.Create("tester", &services.CreateProjectRequest{Name: ptrString("Project A"), Code: "PRJ-A"})
	if err != nil {
		t.Fatalf("create project: %v", err)
	}

	cfg, err := configService.Create("tester", &services.CreateConfigRequest{
		Name:        "Cfg A",
		Items:       models.ConfigItems{{LotName: "CPU_A", Quantity: 1, UnitPrice: 1000}},
		ServerCount: 1,
		ProjectUUID: &project.UUID,
	})
	if err != nil {
		t.Fatalf("create config: %v", err)
	}

	pushService := syncsvc.NewServiceWithDB(serverDB, local)
	pushed, err := pushService.PushPendingChanges()
	if err != nil {
		t.Fatalf("push pending changes: %v", err)
	}
	if pushed < 2 {
		t.Fatalf("expected at least 2 pushed changes, got %d", pushed)
	}

	var serverProject models.Project
	if err := serverDB.Where("uuid = ?", project.UUID).First(&serverProject).Error; err != nil {
		t.Fatalf("project not pushed to server: %v", err)
	}

	var serverCfg models.Configuration
	if err := serverDB.Where("uuid = ?", cfg.UUID).First(&serverCfg).Error; err != nil {
		t.Fatalf("configuration not pushed to server: %v", err)
	}
	if serverCfg.ProjectUUID == nil || *serverCfg.ProjectUUID != project.UUID {
		t.Fatalf("expected project_uuid=%s on pushed config, got %v", project.UUID, serverCfg.ProjectUUID)
	}

	if got := local.CountPendingChanges(); got != 0 {
		t.Fatalf("expected pending queue to be empty, got %d", got)
	}
}

func TestPushPendingChangesProjectCreateThenUpdateBeforeFirstPush(t *testing.T) {
	local := newLocalDBForSyncTest(t)
	serverDB := newServerDBForSyncTest(t)

	localSync := syncsvc.NewService(nil, local)
	projectService := services.NewProjectService(local)
	configService := services.NewLocalConfigurationService(local, localSync, &services.QuoteService{}, func() bool { return false })
	pushService := syncsvc.NewServiceWithDB(serverDB, local)

	project, err := projectService.Create("tester", &services.CreateProjectRequest{Name: ptrString("Project v1"), Code: "PRJ-V1"})
	if err != nil {
		t.Fatalf("create project: %v", err)
	}
	if _, err := projectService.Update(project.UUID, "tester", &services.UpdateProjectRequest{Name: ptrString("Project v2")}); err != nil {
		t.Fatalf("update project: %v", err)
	}

	cfg, err := configService.Create("tester", &services.CreateConfigRequest{
		Name:        "Cfg linked",
		Items:       models.ConfigItems{{LotName: "CPU_A", Quantity: 1, UnitPrice: 1000}},
		ServerCount: 1,
		ProjectUUID: &project.UUID,
	})
	if err != nil {
		t.Fatalf("create config: %v", err)
	}

	if _, err := pushService.PushPendingChanges(); err != nil {
		t.Fatalf("push pending changes: %v", err)
	}

	var serverProject models.Project
	if err := serverDB.Where("uuid = ?", project.UUID).First(&serverProject).Error; err != nil {
		t.Fatalf("project not pushed to server: %v", err)
	}
	if serverProject.Name == nil || *serverProject.Name != "Project v2" {
		t.Fatalf("expected latest project name, got %v", serverProject.Name)
	}

	var serverCfg models.Configuration
	if err := serverDB.Where("uuid = ?", cfg.UUID).First(&serverCfg).Error; err != nil {
		t.Fatalf("configuration not pushed to server: %v", err)
	}
	if serverCfg.ProjectUUID == nil || *serverCfg.ProjectUUID != project.UUID {
		t.Fatalf("expected project_uuid=%s on pushed config, got %v", project.UUID, serverCfg.ProjectUUID)
	}
}

func TestPushPendingChangesSkipsStaleUpdateAndAppliesLatest(t *testing.T) {
	local := newLocalDBForSyncTest(t)
	serverDB := newServerDBForSyncTest(t)

	localSync := syncsvc.NewService(nil, local)
	configService := services.NewLocalConfigurationService(local, localSync, &services.QuoteService{}, func() bool { return false })
	pushService := syncsvc.NewServiceWithDB(serverDB, local)

	created, err := configService.Create("tester", &services.CreateConfigRequest{
		Name:        "Cfg v1",
		Items:       models.ConfigItems{{LotName: "CPU_A", Quantity: 1, UnitPrice: 1000}},
		ServerCount: 1,
	})
	if err != nil {
		t.Fatalf("create config: %v", err)
	}
	if _, err := pushService.PushPendingChanges(); err != nil {
		t.Fatalf("initial push: %v", err)
	}

	if _, err := configService.UpdateNoAuth(created.UUID, &services.CreateConfigRequest{
		Name:        "Cfg v2",
		Items:       models.ConfigItems{{LotName: "CPU_A", Quantity: 2, UnitPrice: 1000}},
		ServerCount: 1,
		ProjectUUID: created.ProjectUUID,
	}); err != nil {
		t.Fatalf("update config: %v", err)
	}

	localCfg, err := local.GetConfigurationByUUID(created.UUID)
	if err != nil {
		t.Fatalf("get local config: %v", err)
	}
	cfgSnapshot := localdb.LocalToConfiguration(localCfg)
	stalePayload := syncsvc.ConfigurationChangePayload{
		EventID:           "stale-event",
		IdempotencyKey:    fmt.Sprintf("%s:v1:update", created.UUID),
		ConfigurationUUID: created.UUID,
		ProjectUUID:       cfgSnapshot.ProjectUUID,
		Operation:         "update",
		CurrentVersionID:  "stale-v1",
		CurrentVersionNo:  1,
		ConflictPolicy:    "last_write_wins",
		Snapshot:          *cfgSnapshot,
		CreatedAt:         time.Now().UTC().Add(-2 * time.Second),
	}
	raw, err := json.Marshal(stalePayload)
	if err != nil {
		t.Fatalf("marshal stale payload: %v", err)
	}
	if err := local.DB().Create(&localdb.PendingChange{
		EntityType: "configuration",
		EntityUUID: created.UUID,
		Operation:  "update",
		Payload:    string(raw),
		CreatedAt:  time.Now().Add(-1 * time.Second),
	}).Error; err != nil {
		t.Fatalf("insert stale pending change: %v", err)
	}

	if _, err := pushService.PushPendingChanges(); err != nil {
		t.Fatalf("push pending with stale event: %v", err)
	}

	var serverCfg models.Configuration
	if err := serverDB.Where("uuid = ?", created.UUID).First(&serverCfg).Error; err != nil {
		t.Fatalf("get server config: %v", err)
	}
	if serverCfg.Name != "Cfg v2" {
		t.Fatalf("expected latest name to win, got %q", serverCfg.Name)
	}
	if got := local.CountPendingChanges(); got != 0 {
		t.Fatalf("expected empty pending queue, got %d", got)
	}
}

func TestPushPendingChangesCreateIsIdempotent(t *testing.T) {
	local := newLocalDBForSyncTest(t)
	serverDB := newServerDBForSyncTest(t)

	localSync := syncsvc.NewService(nil, local)
	configService := services.NewLocalConfigurationService(local, localSync, &services.QuoteService{}, func() bool { return false })
	pushService := syncsvc.NewServiceWithDB(serverDB, local)

	created, err := configService.Create("tester", &services.CreateConfigRequest{
		Name:        "Cfg Idempotent",
		Items:       models.ConfigItems{{LotName: "CPU_B", Quantity: 1, UnitPrice: 500}},
		ServerCount: 1,
	})
	if err != nil {
		t.Fatalf("create config: %v", err)
	}
	if _, err := pushService.PushPendingChanges(); err != nil {
		t.Fatalf("initial push: %v", err)
	}

	localCfg, err := local.GetConfigurationByUUID(created.UUID)
	if err != nil {
		t.Fatalf("get local config: %v", err)
	}
	currentVersionNo, currentVersionID := getCurrentVersionInfo(t, local, created.UUID, localCfg.CurrentVersionID)
	cfgSnapshot := localdb.LocalToConfiguration(localCfg)
	duplicatePayload := syncsvc.ConfigurationChangePayload{
		EventID:           "duplicate-create-event",
		IdempotencyKey:    fmt.Sprintf("%s:v%d:create", created.UUID, currentVersionNo),
		ConfigurationUUID: created.UUID,
		ProjectUUID:       cfgSnapshot.ProjectUUID,
		Operation:         "create",
		CurrentVersionID:  currentVersionID,
		CurrentVersionNo:  currentVersionNo,
		ConflictPolicy:    "last_write_wins",
		Snapshot:          *cfgSnapshot,
		CreatedAt:         time.Now().UTC(),
	}
	raw, err := json.Marshal(duplicatePayload)
	if err != nil {
		t.Fatalf("marshal duplicate payload: %v", err)
	}
	if err := local.AddPendingChange("configuration", created.UUID, "create", string(raw)); err != nil {
		t.Fatalf("add duplicate create pending change: %v", err)
	}

	if pushed, err := pushService.PushPendingChanges(); err != nil {
		t.Fatalf("push duplicate create: %v", err)
	} else if pushed != 1 {
		t.Fatalf("expected 1 pushed change for duplicate create, got %d", pushed)
	}

	var count int64
	if err := serverDB.Model(&models.Configuration{}).Where("uuid = ?", created.UUID).Count(&count).Error; err != nil {
		t.Fatalf("count server configs: %v", err)
	}
	if count != 1 {
		t.Fatalf("expected one server row after idempotent create, got %d", count)
	}
}

func TestPushPendingChangesConfigurationPushesLine(t *testing.T) {
	local := newLocalDBForSyncTest(t)
	serverDB := newServerDBForSyncTest(t)

	localSync := syncsvc.NewService(nil, local)
	configService := services.NewLocalConfigurationService(local, localSync, &services.QuoteService{}, func() bool { return false })
	pushService := syncsvc.NewServiceWithDB(serverDB, local)

	created, err := configService.Create("tester", &services.CreateConfigRequest{
		Name:        "Cfg Line Push",
		Items:       models.ConfigItems{{LotName: "CPU_LINE", Quantity: 1, UnitPrice: 1000}},
		ServerCount: 1,
	})
	if err != nil {
		t.Fatalf("create config: %v", err)
	}
	if created.Line != 10 {
		t.Fatalf("expected local create line=10, got %d", created.Line)
	}

	if _, err := pushService.PushPendingChanges(); err != nil {
		t.Fatalf("push pending changes: %v", err)
	}

	var serverCfg models.Configuration
	if err := serverDB.Where("uuid = ?", created.UUID).First(&serverCfg).Error; err != nil {
		t.Fatalf("load server config: %v", err)
	}
	if serverCfg.Line != 10 {
		t.Fatalf("expected server line=10 after push, got %d", serverCfg.Line)
	}
}

func TestImportConfigurationsToLocalPullsLine(t *testing.T) {
	local := newLocalDBForSyncTest(t)
	serverDB := newServerDBForSyncTest(t)

	cfg := models.Configuration{
		UUID:          "server-line-config",
		OwnerUsername: "tester",
		Name:          "Cfg Line Pull",
		Items:         models.ConfigItems{{LotName: "CPU_PULL", Quantity: 1, UnitPrice: 900}},
		ServerCount:   1,
		Line:          40,
	}
	total := cfg.Items.Total()
	cfg.TotalPrice = &total
	if err := serverDB.Create(&cfg).Error; err != nil {
		t.Fatalf("seed server config: %v", err)
	}

	svc := syncsvc.NewServiceWithDB(serverDB, local)
	if _, err := svc.ImportConfigurationsToLocal(); err != nil {
		t.Fatalf("import configurations to local: %v", err)
	}

	localCfg, err := local.GetConfigurationByUUID(cfg.UUID)
	if err != nil {
		t.Fatalf("load local config: %v", err)
	}
	if localCfg.Line != 40 {
		t.Fatalf("expected imported line=40, got %d", localCfg.Line)
	}
}

func TestImportConfigurationsToLocalLoadsVendorSpecFromServer(t *testing.T) {
	local := newLocalDBForSyncTest(t)
	serverDB := newServerDBForSyncTest(t)

	serverSpec := models.VendorSpec{
		{
			SortOrder:        10,
			VendorPartnumber: "GPU-NVHGX-H200-8141",
			Quantity:         1,
			Description:      "NVIDIA HGX Delta-Next GPU Baseboard",
			LotMappings: []models.VendorSpecLotMapping{
				{LotName: "GPU_NV_H200_141GB_SXM_(HGX)", QuantityPerPN: 8},
			},
		},
	}
	cfg := models.Configuration{
		UUID:          "server-vendorspec-config",
		OwnerUsername: "tester",
		Name:          "Cfg VendorSpec Pull",
		Items:         models.ConfigItems{{LotName: "CPU_PULL", Quantity: 1, UnitPrice: 900}},
		ServerCount:   1,
		Line:          50,
		VendorSpec:    serverSpec,
	}
	total := cfg.Items.Total()
	cfg.TotalPrice = &total
	if err := serverDB.Create(&cfg).Error; err != nil {
		t.Fatalf("seed server config: %v", err)
	}

	svc := syncsvc.NewServiceWithDB(serverDB, local)
	if _, err := svc.ImportConfigurationsToLocal(); err != nil {
		t.Fatalf("import configurations to local: %v", err)
	}

	localCfg, err := local.GetConfigurationByUUID(cfg.UUID)
	if err != nil {
		t.Fatalf("load local config: %v", err)
	}
	if len(localCfg.VendorSpec) != 1 {
		t.Fatalf("expected server vendor_spec imported, got %d rows", len(localCfg.VendorSpec))
	}
	if localCfg.VendorSpec[0].VendorPartnumber != "GPU-NVHGX-H200-8141" {
		t.Fatalf("unexpected vendor_partnumber after import: %q", localCfg.VendorSpec[0].VendorPartnumber)
	}
	if len(localCfg.VendorSpec[0].LotMappings) != 1 || localCfg.VendorSpec[0].LotMappings[0].LotName != "GPU_NV_H200_141GB_SXM_(HGX)" {
		t.Fatalf("unexpected lot mappings after import: %+v", localCfg.VendorSpec[0].LotMappings)
	}
}

func TestPushPendingChangesCreateThenUpdateBeforeFirstPush(t *testing.T) {
	local := newLocalDBForSyncTest(t)
	serverDB := newServerDBForSyncTest(t)

	localSync := syncsvc.NewService(nil, local)
	configService := services.NewLocalConfigurationService(local, localSync, &services.QuoteService{}, func() bool { return false })
	pushService := syncsvc.NewServiceWithDB(serverDB, local)

	created, err := configService.Create("tester", &services.CreateConfigRequest{
		Name:        "Cfg v1",
		Items:       models.ConfigItems{{LotName: "CPU_X", Quantity: 1, UnitPrice: 700}},
		ServerCount: 1,
	})
	if err != nil {
		t.Fatalf("create config: %v", err)
	}

	if _, err := configService.UpdateNoAuth(created.UUID, &services.CreateConfigRequest{
		Name:        "Cfg v2",
		Items:       models.ConfigItems{{LotName: "CPU_X", Quantity: 3, UnitPrice: 700}},
		ServerCount: 1,
		ProjectUUID: created.ProjectUUID,
	}); err != nil {
		t.Fatalf("update config before first push: %v", err)
	}

	pushed, err := pushService.PushPendingChanges()
	if err != nil {
		t.Fatalf("push pending changes: %v", err)
	}
	if pushed < 1 {
		t.Fatalf("expected at least one pushed change, got %d", pushed)
	}

	var serverCfg models.Configuration
	if err := serverDB.Where("uuid = ?", created.UUID).First(&serverCfg).Error; err != nil {
		t.Fatalf("configuration not pushed to server: %v", err)
	}
	if serverCfg.Name != "Cfg v2" {
		t.Fatalf("expected latest update to be pushed, got %q", serverCfg.Name)
	}

	localCfg, err := local.GetConfigurationByUUID(created.UUID)
	if err != nil {
		t.Fatalf("get local config: %v", err)
	}
	if localCfg.ServerID == nil || *localCfg.ServerID == 0 {
		t.Fatalf("expected local configuration to have server_id after push, got %+v", localCfg.ServerID)
	}
}

func newLocalDBForSyncTest(t *testing.T) *localdb.LocalDB {
	t.Helper()
	localPath := filepath.Join(t.TempDir(), "local.db")
	local, err := localdb.New(localPath)
	if err != nil {
		t.Fatalf("init local db: %v", err)
	}
	t.Cleanup(func() { _ = local.Close() })
	return local
}

func newServerDBForSyncTest(t *testing.T) *gorm.DB {
	t.Helper()
	serverPath := filepath.Join(t.TempDir(), "server.db")
	db, err := gorm.Open(sqlite.Open(serverPath), &gorm.Config{})
	if err != nil {
		t.Fatalf("open server sqlite: %v", err)
	}
	if err := db.AutoMigrate(
		&models.Project{},
		&models.Configuration{},
		&models.Pricelist{},
		&models.PricelistItem{},
		&models.Lot{},
	); err != nil {
		t.Fatalf("migrate server test schema: %v", err)
	}
	return db
}

func ptrString(value string) *string {
	return &value
}

func getCurrentVersionInfo(t *testing.T, local *localdb.LocalDB, configurationUUID string, currentVersionID *string) (int, string) {
	t.Helper()
	if currentVersionID == nil || *currentVersionID == "" {
		t.Fatalf("current version id is empty for %s", configurationUUID)
	}

	var version localdb.LocalConfigurationVersion
	if err := local.DB().
		Where("id = ? AND configuration_uuid = ?", *currentVersionID, configurationUUID).
		First(&version).Error; err != nil {
		t.Fatalf("get current version info: %v", err)
	}

	return version.VersionNo, version.ID
}
