package settings

import (
	"io"
	"log"
	"path/filepath"
	"reflect"
	"testing"
)

// newTestSettings returns a Settings backed by a writable temp file so save() works.
func newTestSettings(t *testing.T, paths []StoragePath) *Settings {
	t.Helper()
	logger := log.New(io.Discard, "", 0)
	s, err := Load(filepath.Join(t.TempDir(), "settings.json"), logger)
	if err != nil {
		t.Fatalf("Load: %v", err)
	}
	s.StoragePaths = paths
	return s
}

// TestAutoDiscoverStoragePaths_Additive covers the additive registration behaviour and
// its invariants: existing entries are never mutated, decommissioned entries are never
// reactivated, and IsDefault is never promoted over an existing default.
func TestAutoDiscoverStoragePaths_Additive(t *testing.T) {
	logger := log.New(io.Discard, "", 0)

	tests := []struct {
		name        string
		existing    []StoragePath
		discovered  []string
		fallback    string
		wantPaths   []string // expected Path set after discovery (order-insensitive)
		wantNewPath string   // a path expected to be newly added (may be "")
		// assertions run against the resulting registry
		check func(t *testing.T, s *Settings, before []StoragePath)
	}{
		{
			name: "non-empty registry registers only the missing deployed path",
			existing: []StoragePath{
				{Path: "/mnt/felhom-usb", Label: "Külső tárhely (felhom-usb)", IsDefault: true, Schedulable: true, AddedAt: "2026-01-01T00:00:00Z"},
			},
			discovered:  []string{"/mnt/felhom-usb", "/mnt/hdd_2"}, // existing + a new one
			wantPaths:   []string{"/mnt/felhom-usb", "/mnt/hdd_2"},
			wantNewPath: "/mnt/hdd_2",
			check: func(t *testing.T, s *Settings, before []StoragePath) {
				// pre-existing entry must be byte-identical (incl. IsDefault unchanged)
				got := findPath(s, "/mnt/felhom-usb")
				if got == nil {
					t.Fatalf("pre-existing path vanished")
				}
				if !reflect.DeepEqual(*got, before[0]) {
					t.Errorf("pre-existing entry mutated:\n before=%+v\n after =%+v", before[0], *got)
				}
				// the newly-added path must NOT have stolen default
				nw := findPath(s, "/mnt/hdd_2")
				if nw == nil {
					t.Fatalf("new path /mnt/hdd_2 not registered")
				}
				if nw.IsDefault {
					t.Errorf("new path promoted to default over existing default")
				}
				if !nw.Schedulable {
					t.Errorf("new path should be schedulable")
				}
			},
		},
		{
			name: "registry with no default lets first new path become default",
			existing: []StoragePath{
				{Path: "/mnt/felhom-usb", Label: "x", IsDefault: false, Schedulable: true, AddedAt: "2026-01-01T00:00:00Z"},
			},
			discovered:  []string{"/mnt/hdd_2", "/mnt/hdd_3"},
			wantPaths:   []string{"/mnt/felhom-usb", "/mnt/hdd_2", "/mnt/hdd_3"},
			wantNewPath: "/mnt/hdd_2",
			check: func(t *testing.T, s *Settings, before []StoragePath) {
				d2 := findPath(s, "/mnt/hdd_2")
				d3 := findPath(s, "/mnt/hdd_3")
				if d2 == nil || d3 == nil {
					t.Fatalf("new paths not registered")
				}
				if !d2.IsDefault {
					t.Errorf("first new path should become default when registry has none")
				}
				if d3.IsDefault {
					t.Errorf("only one new path may become default")
				}
			},
		},
		{
			name:        "empty registry behaves like the original (first becomes default)",
			existing:    nil,
			discovered:  []string{"/mnt/felhom-usb", "/mnt/hdd_2"},
			wantPaths:   []string{"/mnt/felhom-usb", "/mnt/hdd_2"},
			wantNewPath: "/mnt/felhom-usb",
			check: func(t *testing.T, s *Settings, before []StoragePath) {
				first := findPath(s, "/mnt/felhom-usb")
				if first == nil || !first.IsDefault {
					t.Errorf("first discovered path should be default in an empty registry")
				}
			},
		},
		{
			name: "fallback path registered when missing",
			existing: []StoragePath{
				{Path: "/mnt/felhom-usb", Label: "x", IsDefault: true, Schedulable: true, AddedAt: "2026-01-01T00:00:00Z"},
			},
			discovered:  nil,
			fallback:    "/mnt/legacy_hdd",
			wantPaths:   []string{"/mnt/felhom-usb", "/mnt/legacy_hdd"},
			wantNewPath: "/mnt/legacy_hdd",
			check:       nil,
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			s := newTestSettings(t, cloneStoragePaths(tc.existing))
			before := cloneStoragePaths(tc.existing)

			s.AutoDiscoverStoragePaths(tc.discovered, tc.fallback, logger)

			// Normalize with filepath.Clean so comparisons hold on both Linux (the deploy
			// target) and Windows (the dev machine), where Clean uses backslashes.
			gotSet := map[string]bool{}
			for _, sp := range s.StoragePaths {
				gotSet[filepath.Clean(sp.Path)] = true
			}
			if len(gotSet) != len(tc.wantPaths) {
				t.Fatalf("path count = %d, want %d (%v)", len(gotSet), len(tc.wantPaths), pathList(s))
			}
			for _, w := range tc.wantPaths {
				if !gotSet[filepath.Clean(w)] {
					t.Errorf("missing expected path %q (have %v)", w, pathList(s))
				}
			}
			if tc.wantNewPath != "" && findPath(s, tc.wantNewPath) == nil {
				t.Errorf("expected new path %q to be registered", tc.wantNewPath)
			}
			if tc.check != nil {
				tc.check(t, s, before)
			}
		})
	}
}

// TestAutoDiscoverStoragePaths_DecommissionedNotReactivated is the companion guard: a
// decommissioned path that is still referenced by a deployed app must NOT be re-added or
// reactivated. This test FAILS if the skip-by-presence guard is removed.
func TestAutoDiscoverStoragePaths_DecommissionedNotReactivated(t *testing.T) {
	logger := log.New(io.Discard, "", 0)
	existing := []StoragePath{
		{Path: "/mnt/old_hdd", Label: "Külső tárhely (old_hdd)", IsDefault: false, Schedulable: true, AddedAt: "2026-01-01T00:00:00Z", Decommissioned: true, DecommissionedAt: "2026-02-01T00:00:00Z", MigratedTo: "/mnt/felhom-usb"},
		{Path: "/mnt/felhom-usb", Label: "x", IsDefault: true, Schedulable: true, AddedAt: "2026-01-01T00:00:00Z"},
	}
	s := newTestSettings(t, cloneStoragePaths(existing))
	before := cloneStoragePaths(existing)

	// A deployed app still points at the decommissioned drive.
	s.AutoDiscoverStoragePaths([]string{"/mnt/old_hdd", "/mnt/felhom-usb"}, "", logger)

	if len(s.StoragePaths) != 2 {
		t.Fatalf("path count changed: got %d want 2 (%v)", len(s.StoragePaths), pathList(s))
	}
	got := findPath(s, "/mnt/old_hdd")
	if got == nil {
		t.Fatalf("decommissioned path vanished")
	}
	if !got.Decommissioned {
		t.Errorf("decommissioned path was REACTIVATED (skip-by-presence guard missing)")
	}
	if !reflect.DeepEqual(*got, before[0]) {
		t.Errorf("decommissioned entry mutated:\n before=%+v\n after =%+v", before[0], *got)
	}
}

func TestInferStorageLabel(t *testing.T) {
	tests := []struct {
		path string
		want string
	}{
		{"/mnt/sys_drive/felhom-data", "Belső SSD (rendszer)"},
		{"/var/lib/felhom/felhom-data", "Belső SSD (rendszer)"},
		{"/mnt/felhom-usb", "Tárhely (felhom-usb)"}, // "felhom-usb" doesn't start with "usb"
		{"/mnt/hdd_1", "Külső tárhely (hdd_1)"},
		{"/mnt/ssd_data", "Külső tárhely (ssd_data)"},
		{"/srv/backups", "Tárhely (backups)"},
	}
	for _, tc := range tests {
		if got := InferStorageLabel(tc.path); got != tc.want {
			t.Errorf("InferStorageLabel(%q) = %q, want %q", tc.path, got, tc.want)
		}
	}
}

// --- helpers ---

func cloneStoragePaths(in []StoragePath) []StoragePath {
	if in == nil {
		return nil
	}
	out := make([]StoragePath, len(in))
	copy(out, in)
	for i := range out {
		if in[i].StoppedStacks != nil {
			out[i].StoppedStacks = append([]string(nil), in[i].StoppedStacks...)
		}
	}
	return out
}

func findPath(s *Settings, path string) *StoragePath {
	want := filepath.Clean(path)
	for i := range s.StoragePaths {
		if filepath.Clean(s.StoragePaths[i].Path) == want {
			return &s.StoragePaths[i]
		}
	}
	return nil
}

func pathList(s *Settings) []string {
	var out []string
	for _, sp := range s.StoragePaths {
		out = append(out, sp.Path)
	}
	return out
}
