package picker

import (
	"testing"

	"git.secluded.site/keld/internal/restic"
)

// testSelection builds a Selection from a small set of LsNodes.
// Tree structure:
//
//	music/
//	  album-01/
//	    01-track.flac
//	    02-track.flac
//	  album-02/
//	    01-song.flac
//	docs/
//	  notes.txt
//	empty/
//	readme.txt
func testSelection(t *testing.T) *Selection {
	t.Helper()
	nodes := []restic.LsNode{
		{Name: "music", Type: "dir", Path: "/music"},
		{Name: "album-01", Type: "dir", Path: "/music/album-01"},
		{Name: "01-track.flac", Type: "file", Path: "/music/album-01/01-track.flac", Size: 1000},
		{Name: "02-track.flac", Type: "file", Path: "/music/album-01/02-track.flac", Size: 2000},
		{Name: "album-02", Type: "dir", Path: "/music/album-02"},
		{Name: "01-song.flac", Type: "file", Path: "/music/album-02/01-song.flac", Size: 3000},
		{Name: "docs", Type: "dir", Path: "/docs"},
		{Name: "notes.txt", Type: "file", Path: "/docs/notes.txt", Size: 500},
		{Name: "empty", Type: "dir", Path: "/empty"},
		{Name: "readme.txt", Type: "file", Path: "/readme.txt", Size: 100},
	}
	sfs := restic.NewSnapshotFS(nodes)
	return NewSelection(sfs)
}

func TestSelectionInitialState(t *testing.T) {
	t.Parallel()
	sel := testSelection(t)

	// Everything starts unselected.
	if sel.State(".") != CheckNone {
		t.Errorf("root: got %v, want CheckNone", sel.State("."))
	}
	if sel.State("music") != CheckNone {
		t.Errorf("music: got %v, want CheckNone", sel.State("music"))
	}
	if sel.State("readme.txt") != CheckNone {
		t.Errorf("readme.txt: got %v, want CheckNone", sel.State("readme.txt"))
	}
	if sel.AllSelected() {
		t.Error("AllSelected should be false initially")
	}
	if paths := sel.SelectedPaths(); len(paths) != 0 {
		t.Errorf("SelectedPaths: got %v, want empty", paths)
	}
}

func TestSelectionToggleFile(t *testing.T) {
	t.Parallel()
	sel := testSelection(t)

	sel.Toggle("readme.txt")
	if sel.State("readme.txt") != CheckAll {
		t.Errorf("readme.txt after toggle: got %v, want CheckAll", sel.State("readme.txt"))
	}
	// Root should be partial now.
	if sel.State(".") != CheckPartial {
		t.Errorf("root after file toggle: got %v, want CheckPartial", sel.State("."))
	}

	// Toggle again to deselect.
	sel.Toggle("readme.txt")
	if sel.State("readme.txt") != CheckNone {
		t.Errorf("readme.txt after second toggle: got %v, want CheckNone", sel.State("readme.txt"))
	}
	if sel.State(".") != CheckNone {
		t.Errorf("root after deselect: got %v, want CheckNone", sel.State("."))
	}
}

func TestSelectionToggleDirectory(t *testing.T) {
	t.Parallel()
	sel := testSelection(t)

	// Selecting album-01 should select both tracks.
	sel.Toggle("music/album-01")
	if sel.State("music/album-01") != CheckAll {
		t.Errorf("album-01: got %v, want CheckAll", sel.State("music/album-01"))
	}
	if sel.State("music/album-01/01-track.flac") != CheckAll {
		t.Error("01-track.flac should be selected")
	}
	if sel.State("music/album-01/02-track.flac") != CheckAll {
		t.Error("02-track.flac should be selected")
	}
	// music should be partial (album-02 is not selected).
	if sel.State("music") != CheckPartial {
		t.Errorf("music: got %v, want CheckPartial", sel.State("music"))
	}

	// Toggle album-01 again to deselect all.
	sel.Toggle("music/album-01")
	if sel.State("music/album-01") != CheckNone {
		t.Errorf("album-01 after deselect: got %v, want CheckNone", sel.State("music/album-01"))
	}
	if sel.State("music") != CheckNone {
		t.Errorf("music after deselect: got %v, want CheckNone", sel.State("music"))
	}
}

func TestSelectionPartialThenToggle(t *testing.T) {
	t.Parallel()
	sel := testSelection(t)

	// Select one track in album-01.
	sel.Toggle("music/album-01/01-track.flac")
	if sel.State("music/album-01") != CheckPartial {
		t.Errorf("album-01: got %v, want CheckPartial", sel.State("music/album-01"))
	}

	// Toggling the partial directory should select ALL descendants.
	sel.Toggle("music/album-01")
	if sel.State("music/album-01") != CheckAll {
		t.Errorf("album-01 after toggle from partial: got %v, want CheckAll", sel.State("music/album-01"))
	}
	if sel.State("music/album-01/02-track.flac") != CheckAll {
		t.Error("02-track.flac should now be selected")
	}
}

func TestSelectionEmptyDir(t *testing.T) {
	t.Parallel()
	sel := testSelection(t)

	sel.Toggle("empty")
	if sel.State("empty") != CheckAll {
		t.Errorf("empty dir: got %v, want CheckAll", sel.State("empty"))
	}

	sel.Toggle("empty")
	if sel.State("empty") != CheckNone {
		t.Errorf("empty dir after deselect: got %v, want CheckNone", sel.State("empty"))
	}
}

func TestSelectionAllSelected(t *testing.T) {
	t.Parallel()
	sel := testSelection(t)

	// Select everything via root.
	sel.Toggle(".")
	if !sel.AllSelected() {
		t.Error("AllSelected should be true after selecting root")
	}
	if sel.State(".") != CheckAll {
		t.Errorf("root: got %v, want CheckAll", sel.State("."))
	}

	// SelectedPaths should return empty (full restore = no --include).
	if paths := sel.SelectedPaths(); len(paths) != 0 {
		t.Errorf("SelectedPaths when all selected: got %v, want empty", paths)
	}

	// Deselect root.
	sel.Toggle(".")
	if sel.AllSelected() {
		t.Error("AllSelected should be false after deselecting root")
	}
}

func TestSelectionSelectedPaths(t *testing.T) {
	t.Parallel()
	sel := testSelection(t)

	// Select all of album-01 and the single file in docs.
	sel.Toggle("music/album-01")
	sel.Toggle("docs/notes.txt")

	paths := sel.SelectedPaths()
	// album-01 is fully selected → compressed to directory.
	// docs has only one child (notes.txt), so selecting it makes docs
	// fully selected → compressed to directory too.
	want := map[string]bool{
		"music/album-01": true,
		"docs":           true,
	}
	if len(paths) != len(want) {
		t.Fatalf("SelectedPaths: got %v, want keys of %v", paths, want)
	}
	for _, p := range paths {
		if !want[p] {
			t.Errorf("unexpected path %q in SelectedPaths", p)
		}
	}
}

func TestSelectionSelectedPathsCompression(t *testing.T) {
	t.Parallel()
	sel := testSelection(t)

	// Select all of music (both albums).
	sel.Toggle("music")

	paths := sel.SelectedPaths()
	// Should compress to just "music".
	if len(paths) != 1 || paths[0] != "music" {
		t.Errorf("SelectedPaths: got %v, want [music]", paths)
	}
}

func TestSelectionDeselectChildMakesParentPartial(t *testing.T) {
	t.Parallel()
	sel := testSelection(t)

	// Select entire music tree, then deselect one track.
	sel.Toggle("music")
	sel.Toggle("music/album-01/01-track.flac")

	if sel.State("music/album-01") != CheckPartial {
		t.Errorf("album-01: got %v, want CheckPartial", sel.State("music/album-01"))
	}
	if sel.State("music") != CheckPartial {
		t.Errorf("music: got %v, want CheckPartial", sel.State("music"))
	}

	// SelectedPaths should list individual files, not the dir.
	paths := sel.SelectedPaths()
	pathSet := make(map[string]bool, len(paths))
	for _, p := range paths {
		pathSet[p] = true
	}
	// album-01 has only 02-track selected; album-02 is fully selected.
	if !pathSet["music/album-01/02-track.flac"] {
		t.Error("expected music/album-01/02-track.flac in paths")
	}
	if !pathSet["music/album-02"] {
		t.Error("expected music/album-02 (fully selected) in paths")
	}
	if pathSet["music/album-01"] {
		t.Error("music/album-01 should NOT be in paths (only partially selected)")
	}
	if pathSet["music"] {
		t.Error("music should NOT be in paths (only partially selected)")
	}
}
