package main import ( "context" "fmt" "slices" "testing" tea "github.com/charmbracelet/bubbletea" ) func TestHandleAction(t *testing.T) { tests := []struct { name string movies []Movie pos int results map[int]Entry key string wantOK bool wantEntry Entry }{ { name: "skip with n", movies: []Movie{{ID: 1, Title: "Alpha"}}, pos: 0, results: map[int]Entry{}, key: "n", wantOK: true, wantEntry: Entry{ID: 1, Seen: false}, }, { name: "seen with s", movies: []Movie{{ID: 2, Title: "Beta"}}, pos: 0, results: map[int]Entry{}, key: "s", wantOK: true, wantEntry: Entry{ID: 2, Seen: true}, }, { name: "rate with 8", movies: []Movie{{ID: 3, Title: "Gamma"}}, pos: 0, results: map[int]Entry{}, key: "8", wantOK: true, wantEntry: Entry{ID: 3, Rating: 8, Seen: true}, }, { name: "rate 1", movies: []Movie{{ID: 4, Title: "Delta"}}, pos: 0, results: map[int]Entry{}, key: "1", wantOK: true, wantEntry: Entry{ID: 4, Rating: 1, Seen: true}, }, { name: "rate 9", movies: []Movie{{ID: 5, Title: "Epsilon"}}, pos: 0, results: map[int]Entry{}, key: "9", wantOK: true, wantEntry: Entry{ID: 5, Rating: 9, Seen: true}, }, { name: "zero maps to rating 10", movies: []Movie{{ID: 6, Title: "Zero"}}, pos: 0, results: map[int]Entry{}, key: "0", wantOK: true, wantEntry: Entry{ID: 6, Rating: 10, Seen: true}, }, { name: "invalid key returns false", movies: []Movie{{ID: 6, Title: "Zeta"}}, pos: 0, results: map[int]Entry{}, key: "x", wantOK: false, }, { name: "empty key not accepted", movies: []Movie{{ID: 7, Title: "Theta"}}, pos: 0, results: map[int]Entry{}, key: "", wantOK: false, }, { name: "overwrite existing result", movies: []Movie{{ID: 9, Title: "Iota"}}, pos: 0, results: map[int]Entry{9: {ID: 9, Seen: false}}, key: "s", wantOK: true, wantEntry: Entry{ID: 9, Seen: true}, }, { name: "multiple movies, second position", movies: []Movie{{ID: 10, Title: "Kappa"}, {ID: 11, Title: "Lambda"}}, pos: 1, results: map[int]Entry{}, key: "5", wantOK: true, wantEntry: Entry{ID: 11, Rating: 5, Seen: true}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { m := tuiModel{ movies: tt.movies, pos: tt.pos, results: tt.results, } got, ok := m.handleAction(tt.key) if ok != tt.wantOK { t.Fatalf("handleAction(%q) = %v, wantOK %v", tt.key, ok, tt.wantOK) } if !ok { return } gotEntry, exists := got.results[tt.movies[tt.pos].ID] if !exists { t.Fatal("result not stored for movie id") } if gotEntry != tt.wantEntry { t.Errorf("result = %+v, want %+v", gotEntry, tt.wantEntry) } }) } } func TestAdvance(t *testing.T) { tests := []struct { name string model tuiModel wantPos int wantQuit bool wantFetch bool }{ { name: "advances within movies", model: tuiModel{ movies: []Movie{{ID: 1}, {ID: 2}, {ID: 3}}, pos: 0, exhausted: true, }, wantPos: 1, }, { name: "hits end exhausted quits", model: tuiModel{ movies: []Movie{{ID: 1}}, pos: 0, exhausted: true, }, wantPos: 1, wantQuit: true, }, { name: "hits end while fetching waits", model: tuiModel{ movies: []Movie{{ID: 1}}, pos: 0, fetching: true, }, wantPos: 1, }, { name: "exhausted skips pre-fetch", model: tuiModel{ movies: []Movie{{ID: 1}, {ID: 2}}, pos: 0, exhausted: true, }, wantPos: 1, }, { name: "fetching skips pre-fetch", model: tuiModel{ movies: []Movie{{ID: 1}, {ID: 2}}, pos: 0, fetching: true, }, wantPos: 1, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { nm, cmd := tt.model.advanceCmd() if nm.pos != tt.wantPos { t.Errorf("pos = %d, want %d", nm.pos, tt.wantPos) } gotQuit := cmd != nil && fmt.Sprintf("%p", cmd) == fmt.Sprintf("%p", tea.Quit) if gotQuit != tt.wantQuit { t.Errorf("quit = %v, want %v", gotQuit, tt.wantQuit) } gotFetch := cmd != nil && !gotQuit if gotFetch != tt.wantFetch { t.Errorf("fetch cmd = %v, want %v", gotFetch, tt.wantFetch) } }) } } func TestRunViewed(t *testing.T) { tests := []struct { name string skipSet map[int]struct{} skipErr error tuiResults map[int]Entry tuiErr error tuiQuit bool existing []Entry wantErr bool wantWrite []Entry }{ { name: "success with ratings", skipSet: map[int]struct{}{}, tuiResults: map[int]Entry{1: {ID: 1, Rating: 8, Seen: true}, 2: {ID: 2, Rating: 5, Seen: true}}, tuiQuit: true, wantWrite: []Entry{ {ID: 1, Rating: 8, Seen: true}, {ID: 2, Rating: 5, Seen: true}, }, }, { name: "no movies fetched", tuiResults: nil, tuiQuit: true, wantWrite: nil, }, { name: "load skip set error", skipErr: fmt.Errorf("file error"), wantErr: true, }, { name: "tui error", tuiErr: fmt.Errorf("tui error"), tuiResults: nil, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { store := &mockStorer{ entries: tt.existing, skipSet: tt.skipSet, skipErr: tt.skipErr, } tui := &mockTUIRunner{ results: tt.tuiResults, err: tt.tuiErr, quit: tt.tuiQuit, } err := runViewed(context.Background(), tmdbClient{}, store, tui, "test.csv") if (err != nil) != tt.wantErr { t.Fatalf("runViewed() = %v, wantErr %v", err, tt.wantErr) } if !slices.Equal(store.WrittenEntries, tt.wantWrite) { t.Errorf("written entries = %+v, want %+v", store.WrittenEntries, tt.wantWrite) } }) } } type mockTUIRunner struct { results map[int]Entry err error quit bool } func (m *mockTUIRunner) Run(_ *tmdbMovieStream, _ context.Context) (map[int]Entry, bool, error) { return m.results, m.quit, m.err }