package screens

import (
	"errors"
	"sync/atomic"
	"testing"

	tea "charm.land/bubbletea/v2"

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

// testNodes returns LsNode entries matching testFS.
func testNodes() []restic.LsNode {
	return []restic.LsNode{
		{Name: "dir1", Type: "dir", Path: "/dir1", Mode: 0o755},
		{Name: "file1.txt", Type: "file", Path: "/dir1/file1.txt", Size: 5, Mode: 0o644},
		{Name: "file2.txt", Type: "file", Path: "/dir1/file2.txt", Size: 5, Mode: 0o644},
		{Name: "dir2", Type: "dir", Path: "/dir2", Mode: 0o755},
		{Name: "file3.txt", Type: "file", Path: "/dir2/file3.txt", Size: 4, Mode: 0o644},
		{Name: "root.txt", Type: "file", Path: "/root.txt", Size: 4, Mode: 0o644},
	}
}

// simulateFileLoad sends a filesLoadedMsg directly to the screen.
func simulateFileLoad(fp *FilePicker, nodes []restic.LsNode, err error) *FilePicker {
	gen := atomic.LoadInt64(&fp.gen)
	msg := filesLoadedMsg{gen: gen, nodes: nodes, err: err}
	screen, cmd := fp.Update(msg)
	fp = screen.(*FilePicker)
	fp, _ = drain(fp, cmd)
	return fp
}

func initFilePicker(fp *FilePicker) *FilePicker {
	fp.Init()
	screen, _ := fp.Update(tea.WindowSizeMsg{Width: 80, Height: 20})
	return screen.(*FilePicker)
}

func TestFilePickerTitle(t *testing.T) {
	t.Parallel()

	fp := NewFilePicker(nil, func() string { return "abc123" }, testStyles())
	if got := fp.Title(); got != "Select files to restore" {
		t.Errorf("Title() = %q, want %q", got, "Select files to restore")
	}
}

func TestFilePickerSelectionEmpty(t *testing.T) {
	t.Parallel()

	fp := NewFilePicker(nil, func() string { return "abc123" }, testStyles())
	if got := fp.Selection(); got != "" {
		t.Errorf("Selection() before interaction = %q, want empty", got)
	}
}

func TestFilePickerKeyBindings(t *testing.T) {
	t.Parallel()

	fp := NewFilePicker(nil, func() string { return "abc123" }, testStyles())
	if len(fp.KeyBindings()) == 0 {
		t.Fatal("KeyBindings() returned no bindings")
	}
}

func TestFilePickerLoadsAndPresents(t *testing.T) {
	t.Parallel()

	nodes := testNodes()
	loader := func(_ string) ([]restic.LsNode, error) { return nodes, nil }

	fp := NewFilePicker(loader, func() string { return "abc123" }, testStyles())
	fp = initFilePicker(fp)
	fp = simulateFileLoad(fp, nodes, nil)

	// Should be in picking phase. The picker should show files.
	if fp.phase != phaseFilePicking {
		t.Fatalf("phase = %d, want %d (phaseFilePicking)", fp.phase, phaseFilePicking)
	}

	view := fp.View()
	if view == "" {
		t.Error("View() should not be empty in picking phase")
	}
}

func TestFilePickerConfirmAllFiles(t *testing.T) {
	t.Parallel()

	nodes := testNodes()
	loader := func(_ string) ([]restic.LsNode, error) { return nodes, nil }

	fp := NewFilePicker(loader, func() string { return "abc123" }, testStyles())
	fp = initFilePicker(fp)
	fp = simulateFileLoad(fp, nodes, nil)

	// Press enter to confirm with all files selected (default).
	screen, cmd := fp.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
	fp = screen.(*FilePicker)

	if cmd == nil {
		t.Fatal("expected DoneCmd after confirming all files")
	}
	if _, ok := cmd().(ui.DoneMsg); !ok {
		t.Errorf("cmd produced %T, want ui.DoneMsg", cmd())
	}

	// All files selected → no includes.
	if got := fp.Includes(); got != nil {
		t.Errorf("Includes() = %v, want nil for full restore", got)
	}

	if got := fp.Selection(); got != "all files" {
		t.Errorf("Selection() = %q, want %q", got, "all files")
	}
}

func TestFilePickerErrorAutoAdvances(t *testing.T) {
	t.Parallel()

	loader := func(_ string) ([]restic.LsNode, error) {
		return nil, errors.New("connection failed")
	}

	fp := NewFilePicker(loader, func() string { return "abc123" }, testStyles())
	fp = initFilePicker(fp)

	// Simulate error result.
	gen := atomic.LoadInt64(&fp.gen)
	msg := filesLoadedMsg{gen: gen, nodes: nil, err: errors.New("connection failed")}
	screen, _ := fp.Update(msg)
	fp = screen.(*FilePicker)

	// Should show a notice phase, not immediately DoneCmd.
	if fp.phase != phaseFileNotice {
		t.Fatalf("phase = %d, want %d (phaseFileNotice)", fp.phase, phaseFileNotice)
	}
	if fp.notice == "" {
		t.Error("notice should be set on error")
	}

	// Press any key to acknowledge and advance.
	screen, cmd := fp.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
	fp = screen.(*FilePicker)

	if cmd == nil {
		t.Fatal("expected DoneCmd after acknowledging notice")
	}
	if _, ok := cmd().(ui.DoneMsg); !ok {
		t.Errorf("cmd produced %T, want ui.DoneMsg", cmd())
	}

	// No includes — full restore.
	if got := fp.Includes(); got != nil {
		t.Errorf("Includes() = %v, want nil after error", got)
	}
}

func TestFilePickerEmptyNodesAutoAdvances(t *testing.T) {
	t.Parallel()

	loader := func(_ string) ([]restic.LsNode, error) {
		return []restic.LsNode{}, nil
	}

	fp := NewFilePicker(loader, func() string { return "abc123" }, testStyles())
	fp = initFilePicker(fp)

	gen := atomic.LoadInt64(&fp.gen)
	msg := filesLoadedMsg{gen: gen, nodes: []restic.LsNode{}, err: nil}
	screen, _ := fp.Update(msg)
	fp = screen.(*FilePicker)

	// Should show notice phase.
	if fp.phase != phaseFileNotice {
		t.Fatalf("phase = %d, want %d (phaseFileNotice)", fp.phase, phaseFileNotice)
	}

	// Press key to advance.
	screen, cmd := fp.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
	_ = screen.(*FilePicker)

	if cmd == nil {
		t.Fatal("expected DoneCmd after acknowledging empty notice")
	}
	if _, ok := cmd().(ui.DoneMsg); !ok {
		t.Errorf("cmd produced %T, want ui.DoneMsg", cmd())
	}
}

func TestFilePickerNilLoaderAutoAdvances(t *testing.T) {
	t.Parallel()

	fp := NewFilePicker(nil, func() string { return "abc123" }, testStyles())
	cmd := fp.Init()

	// With nil loader, should immediately return DoneCmd.
	if cmd == nil {
		t.Fatal("expected DoneCmd from Init with nil loader")
	}
	if _, ok := cmd().(ui.DoneMsg); !ok {
		t.Errorf("cmd produced %T, want ui.DoneMsg", cmd())
	}
}

func TestFilePickerEscAtRootReturnsBack(t *testing.T) {
	t.Parallel()

	nodes := testNodes()
	loader := func(_ string) ([]restic.LsNode, error) { return nodes, nil }

	fp := NewFilePicker(loader, func() string { return "abc123" }, testStyles())
	fp = initFilePicker(fp)
	fp = simulateFileLoad(fp, nodes, nil)

	// At root directory, Esc should return BackCmd.
	_, cmd := fp.Update(tea.KeyPressMsg{Code: tea.KeyEscape})

	if cmd == nil {
		t.Fatal("expected BackCmd on Esc at root")
	}
	if _, ok := cmd().(ui.BackMsg); !ok {
		t.Errorf("cmd produced %T, want ui.BackMsg", cmd())
	}
}

func TestFilePickerEscDuringLoadingReturnsBack(t *testing.T) {
	t.Parallel()

	loader := func(_ string) ([]restic.LsNode, error) { return nil, nil }

	fp := NewFilePicker(loader, func() string { return "abc123" }, testStyles())
	fp = initFilePicker(fp)

	_, cmd := fp.Update(tea.KeyPressMsg{Code: tea.KeyEscape})

	if cmd == nil {
		t.Fatal("expected BackCmd on Esc during loading")
	}
	if _, ok := cmd().(ui.BackMsg); !ok {
		t.Errorf("cmd produced %T, want ui.BackMsg", cmd())
	}
}

func TestFilePickerStaleLoadIgnored(t *testing.T) {
	t.Parallel()

	nodes := testNodes()
	loader := func(_ string) ([]restic.LsNode, error) { return nodes, nil }

	fp := NewFilePicker(loader, func() string { return "abc123" }, testStyles())
	fp = initFilePicker(fp)

	staleGen := atomic.LoadInt64(&fp.gen)
	fp.Init() // bumps generation

	// Deliver stale result.
	msg := filesLoadedMsg{gen: staleGen, nodes: nodes, err: nil}
	screen, _ := fp.Update(msg)
	fp = screen.(*FilePicker)

	// Should still be loading.
	if fp.phase != phaseFileLoading {
		t.Fatalf("phase = %d, want %d (phaseFileLoading) after stale result", fp.phase, phaseFileLoading)
	}
}

func TestFilePickerIncludesWithPartialSelection(t *testing.T) {
	t.Parallel()

	nodes := testNodes()
	loader := func(_ string) ([]restic.LsNode, error) { return nodes, nil }

	fp := NewFilePicker(loader, func() string { return "abc123" }, testStyles())
	fp = initFilePicker(fp)
	fp = simulateFileLoad(fp, nodes, nil)

	// The picker should have files loaded. Toggle the first item
	// (should be dir1, sorted dirs first) via space.
	screen, cmd := fp.Update(tea.KeyPressMsg{Code: ' ', Text: " "})
	fp = screen.(*FilePicker)
	fp, _ = drain(fp, cmd)

	// Confirm with enter.
	screen, cmd = fp.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
	fp = screen.(*FilePicker)

	if cmd == nil {
		t.Fatal("expected DoneCmd after partial selection confirm")
	}

	// Should have includes (partial selection).
	includes := fp.Includes()
	if includes == nil {
		t.Error("Includes() should not be nil for partial selection")
	}

	// All includes should have leading "/".
	for _, inc := range includes {
		if inc[0] != '/' {
			t.Errorf("include %q should start with /", inc)
		}
	}
}

func TestFilePickerSelectionShowsCount(t *testing.T) {
	t.Parallel()

	nodes := testNodes()
	loader := func(_ string) ([]restic.LsNode, error) { return nodes, nil }

	fp := NewFilePicker(loader, func() string { return "abc123" }, testStyles())
	fp = initFilePicker(fp)
	fp = simulateFileLoad(fp, nodes, nil)

	// Toggle first item and confirm.
	screen, cmd := fp.Update(tea.KeyPressMsg{Code: ' ', Text: " "})
	fp = screen.(*FilePicker)
	fp, _ = drain(fp, cmd)

	screen, _ = fp.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
	fp = screen.(*FilePicker)

	sel := fp.Selection()
	if sel == "" {
		t.Error("Selection() should not be empty after partial selection")
	}
	if sel == "all files" {
		t.Error("Selection() should not be 'all files' for partial selection")
	}
}

func TestFilePickerSnapshotIDFromClosure(t *testing.T) {
	t.Parallel()

	var receivedID string
	loader := func(snapshotID string) ([]restic.LsNode, error) {
		receivedID = snapshotID
		return testNodes(), nil
	}

	fp := NewFilePicker(loader, func() string { return "my-snapshot-id" }, testStyles())
	fp = initFilePicker(fp)

	// The loader should have received the snapshot ID from the closure.
	// Since we're simulating, we need to actually call the loader.
	// In the real flow, Init() creates a Cmd that calls the loader.
	// Let's verify the snapshotID func is called properly by
	// checking the stored value.
	if fp.snapshotID != "my-snapshot-id" {
		t.Errorf("snapshotID = %q, want %q", fp.snapshotID, "my-snapshot-id")
	}

	_ = receivedID // used by the loader
}
