package datastore

import (
	"encoding/xml"
	"os"
	"path/filepath"
	"testing"

	"github.com/gesellix/bose-soundtouch/pkg/models"
)

func TestDataStore(t *testing.T) {
	tempDir, err := os.MkdirTemp("", "st-test-*")
	if err != nil {
		t.Fatal(err)
	}

	defer func() { _ = os.RemoveAll(tempDir) }()

	ds := NewDataStore(tempDir)
	account := "test-account"
	device := "test-device"

	// Test Save/Get DeviceInfo
	info := &models.ServiceDeviceInfo{
		DeviceID:  device,
		Name:      "Test Speaker",
		AccountID: account,
	}

	err = ds.SaveDeviceInfo(account, device, info)
	if err != nil {
		t.Errorf("SaveDeviceInfo failed: %v", err)
	}

	loadedInfo, err := ds.GetDeviceInfo(account, device)
	if err != nil {
		t.Errorf("GetDeviceInfo failed: %v", err)
	}

	if loadedInfo.Name != info.Name {
		t.Errorf("Expected name %s, got %s", info.Name, loadedInfo.Name)
	}

	// Test Presets
	presets := []models.ServicePreset{
		{
			ServiceContentItem: models.ServiceContentItem{
				Name: "Preset 1",
			},
		},
	}

	err = ds.SavePresets(account, device, presets)
	if err != nil {
		t.Errorf("SavePresets failed: %v", err)
	}

	loadedPresets, err := ds.GetPresets(account, device)
	if err != nil {
		t.Errorf("GetPresets failed: %v", err)
	}

	if len(loadedPresets) != 1 || loadedPresets[0].Name != "Preset 1" {
		t.Errorf("Unexpected presets: %+v", loadedPresets)
	}

	// Test Recents
	recents := []models.ServiceRecent{
		{
			ServiceContentItem: models.ServiceContentItem{
				Name: "Recent 1",
			},
		},
	}

	err = ds.SaveRecents(account, device, recents)
	if err != nil {
		t.Errorf("SaveRecents failed: %v", err)
	}

	loadedRecents, err := ds.GetRecents(account, device)
	if err != nil {
		t.Errorf("GetRecents failed: %v", err)
	}

	if len(loadedRecents) != 1 || loadedRecents[0].Name != "Recent 1" {
		t.Errorf("Unexpected recents: %+v", loadedRecents)
	}

	// Test path helpers
	expectedAccountDir := filepath.Join(tempDir, "accounts", account)
	if ds.AccountDir(account) != expectedAccountDir {
		t.Errorf("Expected account dir %s, got %s", expectedAccountDir, ds.AccountDir(account))
	}

	// Test GetAccountInfo with placeholder
	accInfo, err := ds.GetAccountInfo("non-existent")
	if err != nil {
		t.Errorf("GetAccountInfo failed: %v", err)
	}
	if !accInfo.IsPlaceholder {
		t.Errorf("Expected IsPlaceholder to be true for non-existent account")
	}

	// Test SaveAccountInfo and GetAccountInfo
	accountID := "acc-123"
	info2 := &models.ServiceAccountInfo{
		AccountID:         accountID,
		PreferredLanguage: "en",
	}
	err = ds.SaveAccountInfo(accountID, info2)
	if err != nil {
		t.Errorf("SaveAccountInfo failed: %v", err)
	}

	loadedAccInfo, err := ds.GetAccountInfo(accountID)
	if err != nil {
		t.Errorf("GetAccountInfo failed: %v", err)
	}
	if loadedAccInfo.PreferredLanguage != "en" {
		t.Errorf("Expected language en, got %s", loadedAccInfo.PreferredLanguage)
	}
	if loadedAccInfo.IsPlaceholder {
		t.Errorf("Expected IsPlaceholder to be false for existing account")
	}
}

func TestListAllDevices_Empty(t *testing.T) {
	tempDir, err := os.MkdirTemp("", "st-empty-test-*")
	if err != nil {
		t.Fatal(err)
	}

	defer func() { _ = os.RemoveAll(tempDir) }()

	ds := NewDataStore(tempDir)

	// Case 1: DataDir does not exist
	_ = os.RemoveAll(tempDir)

	devices, err := ds.ListAllDevices()
	if err != nil {
		t.Errorf("ListAllDevices should not return error when DataDir does not exist, got %v", err)
	}

	if devices == nil || len(devices) != 0 {
		t.Errorf("Expected empty slice when DataDir does not exist, got %+v", devices)
	}

	// Case 2: DataDir is empty
	_ = os.MkdirAll(tempDir, 0755)

	devices, err = ds.ListAllDevices()
	if err != nil {
		t.Errorf("ListAllDevices failed on empty dir: %v", err)
	}

	if devices == nil {
		t.Errorf("Expected empty slice (not nil) when no devices exist")
	}

	if len(devices) != 0 {
		t.Errorf("Expected 0 devices, got %d", len(devices))
	}
}

func TestListAllDevices(t *testing.T) {
	tempDir, err := os.MkdirTemp("", "st-list-test-*")
	if err != nil {
		t.Fatal(err)
	}

	defer func() { _ = os.RemoveAll(tempDir) }()

	ds := NewDataStore(tempDir)
	account := "default"
	deviceID := "BO5EBO5E-F00D-F00D-FEED-AABBCCDDEE0A"

	info := &models.ServiceDeviceInfo{
		DeviceID:           deviceID,
		Name:               "Test Speaker",
		IPAddress:          "192.0.2.100",
		DeviceSerialNumber: deviceID,
		ProductCode:        "SoundTouch 10",
		FirmwareVersion:    "1.2.3",
		AccountID:          account,
	}

	err = ds.SaveDeviceInfo(account, deviceID, info)
	if err != nil {
		t.Fatalf("SaveDeviceInfo failed: %v", err)
	}

	devices, err := ds.ListAllDevices()
	if err != nil {
		t.Fatalf("ListAllDevices failed: %v", err)
	}

	if len(devices) != 1 {
		t.Fatalf("Expected 1 device, got %d", len(devices))
	}

	if devices[0].DeviceID != deviceID {
		t.Errorf("Expected DeviceID %s, got %s", deviceID, devices[0].DeviceID)
	}
}

func TestListAllDevices_EmptyDeviceID(t *testing.T) {
	tempDir, err := os.MkdirTemp("", "st-empty-id-test-*")
	if err != nil {
		t.Fatal(err)
	}

	defer func() { _ = os.RemoveAll(tempDir) }()

	ds := NewDataStore(tempDir)
	account := "default"
	deviceID := ""

	info := &models.ServiceDeviceInfo{
		DeviceID:  deviceID,
		Name:      "Empty ID Speaker",
		AccountID: account,
	}

	// Use IP as fallback for device ID if it is empty
	key := deviceID
	if key == "" {
		key = "127.0.0.1"
	}

	err = ds.SaveDeviceInfo(account, key, info)
	if err != nil {
		t.Fatalf("SaveDeviceInfo failed: %v", err)
	}

	devices, err := ds.ListAllDevices()
	if err != nil {
		t.Fatalf("ListAllDevices failed: %v", err)
	}

	if len(devices) != 1 {
		t.Fatalf("Expected 1 device, got %d", len(devices))
	}

	if devices[0].Name != "Empty ID Speaker" {
		t.Errorf("Expected Name 'Empty ID Speaker', got %s", devices[0].Name)
	}
}

func TestListAllDevices_MultipleEmptyIDs(t *testing.T) {
	tempDir, err := os.MkdirTemp("", "st-multi-empty-test-*")
	if err != nil {
		t.Fatal(err)
	}

	defer func() { _ = os.RemoveAll(tempDir) }()

	ds := NewDataStore(tempDir)
	account := "default"

	// Save two devices with empty ID but different IPs
	info1 := &models.ServiceDeviceInfo{
		DeviceID:  "",
		Name:      "Speaker 1",
		IPAddress: "192.0.2.1",
		AccountID: account,
	}
	info2 := &models.ServiceDeviceInfo{
		DeviceID:  "",
		Name:      "Speaker 2",
		IPAddress: "192.0.2.2",
		AccountID: account,
	}

	// We use the same logic as in main.go: use IP as fallback for directory name
	err = ds.SaveDeviceInfo(account, info1.IPAddress, info1)
	if err != nil {
		t.Fatalf("SaveDeviceInfo 1 failed: %v", err)
	}

	err = ds.SaveDeviceInfo(account, info2.IPAddress, info2)
	if err != nil {
		t.Fatalf("SaveDeviceInfo 2 failed: %v", err)
	}

	devices, err := ds.ListAllDevices()
	if err != nil {
		t.Fatalf("ListAllDevices failed: %v", err)
	}

	if len(devices) != 2 {
		t.Fatalf("Expected 2 devices, got %d", len(devices))
	}
}

func TestListAllDevices_MalformedXML(t *testing.T) {
	tempDir, err := os.MkdirTemp("", "st-malformed-test-*")
	if err != nil {
		t.Fatal(err)
	}

	defer func() { _ = os.RemoveAll(tempDir) }()

	ds := NewDataStore(tempDir)
	account := "default"
	deviceID := "malformed-device"

	dir := ds.AccountDeviceDir(account, deviceID)
	_ = os.MkdirAll(dir, 0755)
	_ = os.WriteFile(filepath.Join(dir, "DeviceInfo.xml"), []byte("<info>not even closed"), 0644)

	devices, err := ds.ListAllDevices()
	if err != nil {
		t.Fatalf("ListAllDevices should not return error on malformed XML: %v", err)
	}

	if len(devices) != 0 {
		t.Errorf("Expected 0 devices, got %d", len(devices))
	}
}

func TestConfiguredSources(t *testing.T) {
	tempDir, _ := os.MkdirTemp("", "datastore-sources-*")

	defer func() { _ = os.RemoveAll(tempDir) }()

	ds := NewDataStore(tempDir)
	account := "test-acc"

	sources := []models.ConfiguredSource{
		{
			DisplayName: "Source 1",
			ID:          "101",
			Secret:      "secret1",
			SecretType:  "type1",
			SourceKey: struct {
				Type    string `xml:"type,attr"`
				Account string `xml:"account,attr"`
			}{Type: "TUNEIN", Account: "user1"},
			SourceKeyType:    "TUNEIN",
			SourceKeyAccount: "user1",
		},
		{
			DisplayName: "Source 2",
			ID:          "102",
			Secret:      "secret2",
			SecretType:  "type2",
			SourceKey: struct {
				Type    string `xml:"type,attr"`
				Account string `xml:"account,attr"`
			}{Type: "PANDORA", Account: "user2"},
			SourceKeyType:    "PANDORA",
			SourceKeyAccount: "user2",
		},
	}

	err := ds.SaveConfiguredSources(account, "any", sources)
	if err != nil {
		t.Fatalf("SaveConfiguredSources failed: %v", err)
	}

	loadedSources, err := ds.GetConfiguredSources(account, "any")
	if err != nil {
		t.Fatalf("GetConfiguredSources failed: %v", err)
	}

	if len(loadedSources) != len(sources) {
		t.Fatalf("Expected %d sources, got %d", len(sources), len(loadedSources))
	}

	for i := range sources {
		ls := loadedSources[i]
		expected := sources[i]
		expected.Secret = ""
		expected.SecretType = ""
		expected.Type = ls.Type // Ignore Type mismatch in this test if it's auto-populated

		// Clear secrets for comparison since they are not loaded by GetConfiguredSources
		ls.Secret = ""
		ls.SecretType = ""

		if ls.DisplayName != expected.DisplayName || ls.ID != expected.ID || ls.Secret != expected.Secret ||
			ls.SecretType != expected.SecretType || ls.SourceKeyType != expected.SourceKeyType ||
			ls.SourceKeyAccount != expected.SourceKeyAccount || ls.Type != expected.Type {
			// Clean XMLName for comparison
			ls.XMLName = xml.Name{}
			if ls.DisplayName != expected.DisplayName || ls.ID != expected.ID || ls.Secret != expected.Secret ||
				ls.SecretType != expected.SecretType || ls.SourceKeyType != expected.SourceKeyType ||
				ls.SourceKeyAccount != expected.SourceKeyAccount || ls.Type != expected.Type {
				t.Errorf("Source %d mismatch. Expected %+v, got %+v", i, expected, ls)
			}
		}
	}

	// Test with missing ID (GetConfiguredSources should auto-assign)
	sources2 := []models.ConfiguredSource{
		{
			DisplayName:      "Source No ID",
			SourceKeyType:    "LOCAL",
			SourceKeyAccount: "user3",
		},
	}

	err = ds.SaveConfiguredSources(account, "any", sources2)
	if err != nil {
		t.Fatal(err)
	}

	loadedSources2, err := ds.GetConfiguredSources(account, "any")
	if err != nil {
		t.Fatal(err)
	}

	if loadedSources2[0].ID == "" {
		t.Error("Expected auto-assigned ID for source with empty ID")
	}
}

func TestSettingsPersistence(t *testing.T) {
	tempDir, err := os.MkdirTemp("", "settings-test-*")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(tempDir)

	ds := NewDataStore(tempDir)

	settings := Settings{
		ServerURL:           "http://myserver:8000",
		LogBodies:           true,
		DiscoveryInterval:   "10m",
		DiscoveryEnabled:    true,
		UpdateCheckInterval: "12h",
		UpdateCheckEnabled:  true,
	}

	err = ds.SaveSettings(settings)
	if err != nil {
		t.Fatalf("SaveSettings failed: %v", err)
	}

	loaded, err := ds.GetSettings()
	if err != nil {
		t.Fatalf("GetSettings failed: %v", err)
	}

	if loaded.ServerURL != settings.ServerURL {
		t.Errorf("Expected ServerURL %s, got %s", settings.ServerURL, loaded.ServerURL)
	}
	if loaded.LogBodies != settings.LogBodies {
		t.Errorf("Expected LogBodies %v, got %v", settings.LogBodies, loaded.LogBodies)
	}
	if loaded.DiscoveryInterval != settings.DiscoveryInterval {
		t.Errorf("Expected DiscoveryInterval %s, got %s", settings.DiscoveryInterval, loaded.DiscoveryInterval)
	}
	if loaded.DiscoveryEnabled != settings.DiscoveryEnabled {
		t.Errorf("Expected DiscoveryEnabled %v, got %v", settings.DiscoveryEnabled, loaded.DiscoveryEnabled)
	}
	if loaded.UpdateCheckInterval != settings.UpdateCheckInterval {
		t.Errorf("Expected UpdateCheckInterval %s, got %s", settings.UpdateCheckInterval, loaded.UpdateCheckInterval)
	}
	if loaded.UpdateCheckEnabled != settings.UpdateCheckEnabled {
		t.Errorf("Expected UpdateCheckEnabled %v, got %v", settings.UpdateCheckEnabled, loaded.UpdateCheckEnabled)
	}
}

// TestUpdateCheckState_MissingFileReturnsZeroValue verifies a fresh install
// (or one where the update check has never run) gets a zero-value state,
// not an error — same shape as GetSettings on a missing settings.json.
func TestUpdateCheckState_MissingFileReturnsZeroValue(t *testing.T) {
	tempDir, err := os.MkdirTemp("", "update-check-missing-test-*")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(tempDir)

	ds := NewDataStore(tempDir)

	state, err := ds.GetUpdateCheckState()
	if err != nil {
		t.Fatalf("GetUpdateCheckState on a fresh install should not error, got: %v", err)
	}
	if state != (UpdateCheckState{}) {
		t.Errorf("Expected zero-value state, got %+v", state)
	}
}

// TestUpdateCheckState_Persistence is the roundtrip test, mirroring
// TestSettingsPersistence.
func TestUpdateCheckState_Persistence(t *testing.T) {
	tempDir, err := os.MkdirTemp("", "update-check-persist-test-*")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(tempDir)

	ds := NewDataStore(tempDir)

	state := UpdateCheckState{
		LastCheckedAt:   "2026-08-09T12:00:00Z",
		LastSeenVersion: "v0.122.0",
		LastReleaseURL:  "https://github.com/gesellix/Bose-SoundTouch/releases/tag/v0.122.0",
	}

	if err := ds.SaveUpdateCheckState(state); err != nil {
		t.Fatalf("SaveUpdateCheckState failed: %v", err)
	}

	loaded, err := ds.GetUpdateCheckState()
	if err != nil {
		t.Fatalf("GetUpdateCheckState failed: %v", err)
	}

	if loaded != state {
		t.Errorf("Expected %+v, got %+v", state, loaded)
	}
}

// TestRecordActivity_EmptyKindReturnsNilNotError verifies GetActivityRecords
// for a kind that was never recorded returns an empty, non-error result —
// the "nothing recorded yet" case, not a failure.
func TestRecordActivity_EmptyKindReturnsNilNotError(t *testing.T) {
	tempDir, err := os.MkdirTemp("", "activity-empty-test-*")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(tempDir)

	ds := NewDataStore(tempDir)

	records, err := ds.GetActivityRecords("notification_dismissed")
	if err != nil {
		t.Fatalf("GetActivityRecords on empty kind should not error, got: %v", err)
	}
	if len(records) != 0 {
		t.Errorf("Expected no records, got %d", len(records))
	}
}

// TestRecordActivity_SameIDRecursWithNewTimestamp is the regression test for
// the append-only shape agreed in the #419 design: dismissing the same
// announcement twice must produce two records, not overwrite one — this is
// a log, not a keyed map.
func TestRecordActivity_SameIDRecursWithNewTimestamp(t *testing.T) {
	tempDir, err := os.MkdirTemp("", "activity-recur-test-*")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(tempDir)

	ds := NewDataStore(tempDir)

	if err := ds.RecordActivity("notification_dismissed", "admin-gate-notice", nil); err != nil {
		t.Fatalf("First RecordActivity failed: %v", err)
	}
	if err := ds.RecordActivity("notification_dismissed", "admin-gate-notice", nil); err != nil {
		t.Fatalf("Second RecordActivity failed: %v", err)
	}

	records, err := ds.GetActivityRecords("notification_dismissed")
	if err != nil {
		t.Fatalf("GetActivityRecords failed: %v", err)
	}
	if len(records) != 2 {
		t.Fatalf("Expected 2 records for the same recurring id, got %d: %+v", len(records), records)
	}
	for _, r := range records {
		if r.ID != "admin-gate-notice" || r.Kind != "notification_dismissed" || r.Timestamp == "" {
			t.Errorf("Unexpected record shape: %+v", r)
		}
	}
}

// TestRecordActivity_DetailRoundTrips verifies the optional detail payload
// survives a write/read round trip.
func TestRecordActivity_DetailRoundTrips(t *testing.T) {
	tempDir, err := os.MkdirTemp("", "activity-detail-test-*")
	if err != nil {
		t.Fatal(err)
	}
	defer os.RemoveAll(tempDir)

	ds := NewDataStore(tempDir)

	if err := ds.RecordActivity("some_kind", "some-id", map[string]interface{}{"note": "hello"}); err != nil {
		t.Fatalf("RecordActivity failed: %v", err)
	}

	records, err := ds.GetActivityRecords("some_kind")
	if err != nil {
		t.Fatalf("GetActivityRecords failed: %v", err)
	}
	if len(records) != 1 {
		t.Fatalf("Expected 1 record, got %d", len(records))
	}
	if records[0].Detail["note"] != "hello" {
		t.Errorf("Expected detail to round-trip, got: %+v", records[0].Detail)
	}
}

func TestMoveDeviceMigratesData(t *testing.T) {
	tempDir := t.TempDir()
	ds := NewDataStore(tempDir)

	oldAccount := "default"
	newAccount := "8637922"
	deviceID := "F4E11E930BEB"

	// Seed the device under the old account with presets
	presets := []models.ServicePreset{
		{ID: "1", ServiceContentItem: models.ServiceContentItem{Name: "Radio Preset", ContentItemType: "stationurl"}},
	}
	if err := ds.SavePresets(oldAccount, deviceID, presets); err != nil {
		t.Fatalf("SavePresets under %s: %v", oldAccount, err)
	}

	// Move the device to the new account
	if err := ds.MoveDevice(oldAccount, newAccount, deviceID); err != nil {
		t.Fatalf("MoveDevice %s → %s: %v", oldAccount, newAccount, err)
	}

	// Verify presets survived the move under the new account
	moved, err := ds.GetPresets(newAccount, deviceID)
	if err != nil {
		t.Fatalf("GetPresets under %s after move: %v", newAccount, err)
	}
	if len(moved) != 1 || moved[0].Name != "Radio Preset" {
		t.Errorf("presets not preserved: got %+v", moved)
	}

	// Verify the old account no longer has the device
	oldPresets, err := ds.GetPresets(oldAccount, deviceID)
	if err == nil && len(oldPresets) > 0 {
		t.Errorf("old account %s still has presets after move: %+v", oldAccount, oldPresets)
	}
}
