package screens

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

	tea "charm.land/bubbletea/v2"

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

func testSnapshots() []restic.Snapshot {
	return []restic.Snapshot{
		{
			ID:       "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
			ShortID:  "abcdef01",
			Time:     time.Date(2026, 3, 15, 10, 30, 0, 0, time.UTC),
			Hostname: "host1",
			Paths:    []string{"/home"},
			Tags:     []string{"daily"},
		},
		{
			ID:       "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
			ShortID:  "12345678",
			Time:     time.Date(2026, 3, 14, 9, 0, 0, 0, time.UTC),
			Hostname: "host2",
			Paths:    []string{"/etc"},
		},
	}
}

// simulateLoad sends a snapshotsLoadedMsg directly to the screen,
// bypassing tea.Batch. This is how tests deliver async results —
// in the real runtime, the Batch command produces this message.
func simulateLoad(s *Snapshot, snaps []restic.Snapshot, err error) *Snapshot {
	gen := atomic.LoadInt64(&s.gen)
	msg := snapshotsLoadedMsg{gen: gen, snapshots: snaps, err: err}
	screen, cmd := s.Update(msg)
	s = screen.(*Snapshot)
	// Drain any form init commands.
	s, _ = drain(s, cmd)
	return s
}

// simulateStaleLoad sends a snapshotsLoadedMsg with an old generation,
// simulating a result from a prior Init that should be ignored.
func simulateStaleLoad(s *Snapshot, snaps []restic.Snapshot, err error, staleGen int64) *Snapshot {
	msg := snapshotsLoadedMsg{gen: staleGen, snapshots: snaps, err: err}
	screen, _ := s.Update(msg)
	return screen.(*Snapshot)
}

func initSnapshot(s *Snapshot) *Snapshot {
	s.Init()
	screen, _ := s.Update(tea.WindowSizeMsg{Width: 80, Height: 20})
	return screen.(*Snapshot)
}

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

	s := NewSnapshot(nil, testStyles())
	if got := s.Title(); got != "Select a snapshot" {
		t.Errorf("Title() = %q, want %q", got, "Select a snapshot")
	}
}

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

	s := NewSnapshot(nil, testStyles())
	if got := s.Selection(); got != "" {
		t.Errorf("Selection() before interaction = %q, want empty", got)
	}
}

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

	s := NewSnapshot(nil, testStyles())
	if len(s.KeyBindings()) == 0 {
		t.Fatal("KeyBindings() returned no bindings")
	}
}

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

	snaps := testSnapshots()
	loader := func() ([]restic.Snapshot, error) { return snaps, nil }

	s := NewSnapshot(loader, testStyles())
	s = initSnapshot(s)
	s = simulateLoad(s, snaps, nil)

	// Should now be in the selecting phase. Press enter to pick first.
	screen, cmd := s.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
	s, cmd = drain(screen.(*Snapshot), cmd)

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

	if got := s.Value(); got != "abcdef01" {
		t.Errorf("Value() = %q, want %q", got, "abcdef01")
	}
}

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

	snaps := testSnapshots()
	loader := func() ([]restic.Snapshot, error) { return snaps, nil }

	s := NewSnapshot(loader, testStyles())
	s = initSnapshot(s)
	s = simulateLoad(s, snaps, nil)

	// Select first snapshot.
	screen, cmd := s.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
	s, _ = drain(screen.(*Snapshot), cmd)

	if got := s.Selection(); got != "abcdef01" {
		t.Errorf("Selection() = %q, want %q", got, "abcdef01")
	}
}

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

	netErr := errors.New("network error")
	loader := func() ([]restic.Snapshot, error) { return nil, netErr }

	s := NewSnapshot(loader, testStyles())
	s = initSnapshot(s)
	s = simulateLoad(s, nil, netErr)

	// Should be in manual entry phase with error notice.
	if s.notice == "" {
		t.Error("notice should be set on error")
	}

	// Type a snapshot ID and submit.
	for _, ch := range "latest" {
		screen, _ := s.Update(tea.KeyPressMsg{Code: ch, Text: string(ch)})
		s = screen.(*Snapshot)
	}

	screen, cmd := s.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
	s, cmd = drain(screen.(*Snapshot), cmd)

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

	if got := s.Value(); got != "latest" {
		t.Errorf("Value() = %q, want %q", got, "latest")
	}
}

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

	loader := func() ([]restic.Snapshot, error) { return nil, restic.ErrNoRepo }

	s := NewSnapshot(loader, testStyles())
	s = initSnapshot(s)
	s = simulateLoad(s, nil, restic.ErrNoRepo)

	// Should be in manual entry phase with no error notice.
	if s.notice != "" {
		t.Errorf("notice = %q, want empty for ErrNoRepo (silent fallback)", s.notice)
	}

	// Type and submit.
	for _, ch := range "abc123" {
		screen, _ := s.Update(tea.KeyPressMsg{Code: ch, Text: string(ch)})
		s = screen.(*Snapshot)
	}
	screen, cmd := s.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
	s, cmd = drain(screen.(*Snapshot), cmd)

	if cmd == nil {
		t.Fatal("expected DoneCmd after manual entry")
	}
	if got := s.Value(); got != "abc123" {
		t.Errorf("Value() = %q, want %q", got, "abc123")
	}
}

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

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

	s := NewSnapshot(loader, testStyles())
	s = initSnapshot(s)
	s = simulateLoad(s, []restic.Snapshot{}, nil)

	// Should show a notice about empty repo.
	if s.notice == "" {
		t.Error("notice should be set when repository has no snapshots")
	}
}

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

	s := NewSnapshot(nil, testStyles())
	cmd := s.Init()
	screen, _ := s.Update(tea.WindowSizeMsg{Width: 80, Height: 20})
	s = screen.(*Snapshot)

	// Drain any init commands from the manual form.
	s, _ = drain(s, cmd)

	// With no loader, should start in manual entry immediately.
	for _, ch := range "latest" {
		screen, _ := s.Update(tea.KeyPressMsg{Code: ch, Text: string(ch)})
		s = screen.(*Snapshot)
	}
	screen, cmd = s.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
	s, cmd = drain(screen.(*Snapshot), cmd)

	if cmd == nil {
		t.Fatal("expected DoneCmd after manual entry")
	}
	if got := s.Value(); got != "latest" {
		t.Errorf("Value() = %q, want %q", got, "latest")
	}
}

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

	snaps := testSnapshots()
	loader := func() ([]restic.Snapshot, error) { return snaps, nil }

	s := NewSnapshot(loader, testStyles())
	s = initSnapshot(s)
	s = simulateLoad(s, snaps, nil)

	// Navigate down to the "Enter ID manually…" option (last item,
	// 2 snapshots + 1 manual = index 2, so press down twice).
	screen, _ := s.Update(tea.KeyPressMsg{Code: tea.KeyDown})
	s = screen.(*Snapshot)
	screen, _ = s.Update(tea.KeyPressMsg{Code: tea.KeyDown})
	s = screen.(*Snapshot)

	// Select the manual entry option.
	screen, cmd := s.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
	s, _ = drain(screen.(*Snapshot), cmd)

	// Should now be in manual entry phase. Type an ID.
	for _, ch := range "abc123:subfolder" {
		screen, _ := s.Update(tea.KeyPressMsg{Code: ch, Text: string(ch)})
		s = screen.(*Snapshot)
	}
	screen, cmd = s.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
	s, cmd = drain(screen.(*Snapshot), cmd)

	if cmd == nil {
		t.Fatal("expected DoneCmd after manual entry from select")
	}
	if got := s.Value(); got != "abc123:subfolder" {
		t.Errorf("Value() = %q, want %q", got, "abc123:subfolder")
	}
}

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

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

	s := NewSnapshot(loader, testStyles())
	s = initSnapshot(s)

	// Press Esc during loading phase.
	_, cmd := s.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 TestSnapshotEscDuringSelectReturnsBack(t *testing.T) {
	t.Parallel()

	snaps := testSnapshots()
	loader := func() ([]restic.Snapshot, error) { return snaps, nil }

	s := NewSnapshot(loader, testStyles())
	s = initSnapshot(s)
	s = simulateLoad(s, snaps, nil)

	// Esc during selection should back out.
	_, cmd := s.Update(tea.KeyPressMsg{Code: tea.KeyEscape})

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

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

	snaps := testSnapshots()
	loader := func() ([]restic.Snapshot, error) { return snaps, nil }

	s := NewSnapshot(loader, testStyles())
	s = initSnapshot(s)

	// Record the current generation, then re-init to bump it.
	staleGen := atomic.LoadInt64(&s.gen)
	s.Init()

	// Deliver a result with the stale generation. Should be ignored.
	s = simulateStaleLoad(s, snaps, nil, staleGen)

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

	// Deliver with the current generation.
	s = simulateLoad(s, snaps, nil)

	// Now should be in selecting phase.
	screen, cmd := s.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
	_, cmd = drain(screen.(*Snapshot), cmd)

	if cmd == nil {
		t.Fatal("expected DoneCmd after selecting from fresh load")
	}
}
