package decision

import (
	"context"
	"sync"
	"testing"
	"time"
)

func TestFinishNamesTheNeverAsked(t *testing.T) {
	ctx, rec := With(context.Background(), "какая погода в риме?")
	Expect(ctx, StageQuery, []string{"calendar", "weather", "search", "kiwix"})
	Note(ctx, Claim{Stage: StageQuery, Claimant: "calendar", Outcome: Declined})
	Note(ctx, Claim{Stage: StageQuery, Claimant: "weather", Outcome: Won})

	rec.Finish(time.Now())

	outcomes := map[string]string{}
	for _, c := range rec.Claims {
		outcomes[c.Claimant] = c.Outcome
	}
	if outcomes["weather"] != Won || rec.Winner != StageQuery+":weather" {
		t.Errorf("winner = %q, weather = %q", rec.Winner, outcomes["weather"])
	}
	if outcomes["calendar"] != Declined {
		t.Errorf("calendar = %q, want a decline", outcomes["calendar"])
	}
	// The two below the winner never looked, and saying so is the whole point.
	for _, name := range []string{"search", "kiwix"} {
		if outcomes[name] != NeverAsked {
			t.Errorf("%s = %q, want %q", name, outcomes[name], NeverAsked)
		}
	}
}

// A real 0.0 confidence must not read as "this claimant has no score".
func TestScoredKeepsAZeroScore(t *testing.T) {
	c := Scored(StageRoute, "classifier", "chat", 0, LostOnScore, "")
	if !c.HasScore || c.Score != 0 {
		t.Errorf("claim = %+v", c)
	}
}

func TestNoteIfUnclaimedYieldsToARealWinner(t *testing.T) {
	ctx, rec := With(context.Background(), "x")
	Note(ctx, Claim{Stage: StageQuery, Claimant: "weather", Outcome: Won})
	rec.NoteIfUnclaimed(Claim{Stage: StageAction, Claimant: "action-handler"})
	if rec.Winner != StageQuery+":weather" {
		t.Errorf("winner = %q, want the query source", rec.Winner)
	}
}

// A context with no record must cost nothing and crash nothing: that is what
// makes the claim sites safe to leave in every test and every fixture run.
func TestNoRecorderIsANoOp(t *testing.T) {
	ctx := context.Background()
	Note(ctx, Claim{Claimant: "x", Outcome: Won})
	Expect(ctx, StageQuery, []string{"y"})
	if From(ctx) != nil {
		t.Error("bare context reported a record")
	}
	From(ctx).NoteIfUnclaimed(Claim{Claimant: "z"})
	if rec := From(ctx).Finish(time.Now()); rec != nil {
		t.Error("finishing a nil record produced one")
	}
}

func TestRingIsBoundedAndNewestFirst(t *testing.T) {
	r := NewRing()
	for i := 0; i < ringSize+5; i++ {
		r.Push(&Record{Utterance: string(rune('a' + i))})
	}
	got := r.Recent(ringSize + 10)
	if len(got) != ringSize {
		t.Fatalf("kept %d records, want %d", len(got), ringSize)
	}
	if got[0].Utterance != string(rune('a'+ringSize+4)) {
		t.Errorf("newest = %q", got[0].Utterance)
	}
}

// A query source may fan out to goroutines of its own, so two of them noting at
// once must not race. Run under -race, which is where this earns its keep.
func TestConcurrentNotes(t *testing.T) {
	ctx, rec := With(context.Background(), "x")
	var wg sync.WaitGroup
	for i := 0; i < 8; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			Note(ctx, Claim{Stage: StageQuery, Claimant: "fanout", Outcome: Declined})
		}()
	}
	wg.Wait()
	if len(rec.Claims) != 8 {
		t.Errorf("recorded %d claims, want 8", len(rec.Claims))
	}
}
