package main

import (
	"bytes"
	"io"
	"os"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/zzet/gortex/internal/savings"
)

// captureStdout runs fn with os.Stdout redirected to a pipe and returns
// whatever fn wrote. Tests use this to assert against the dashboard
// output without touching the real terminal.
func captureStdout(t *testing.T, fn func()) string {
	t.Helper()
	orig := os.Stdout
	r, w, err := os.Pipe()
	if err != nil {
		t.Fatalf("pipe: %v", err)
	}
	os.Stdout = w

	var buf bytes.Buffer
	var wg sync.WaitGroup
	wg.Go(func() {
		_, _ = io.Copy(&buf, r)
	})

	defer func() {
		os.Stdout = orig
	}()
	fn()
	_ = w.Close()
	wg.Wait()
	return buf.String()
}

func TestPickHeadlineCost_RespectsModelFlag(t *testing.T) {
	costs := map[string]float64{
		"claude-opus-4":   0.50,
		"claude-sonnet-4": 0.10,
		"claude-haiku-4.5": 0.03,
	}
	val, name := pickHeadlineCost(costs, "claude-sonnet-4")
	if name != "claude-sonnet-4" || val != 0.10 {
		t.Errorf("pickHeadlineCost(preferred=sonnet) = (%.2f, %q), want (0.10, claude-sonnet-4)", val, name)
	}
}

func TestPickHeadlineCost_DefaultsToMostExpensive(t *testing.T) {
	costs := map[string]float64{
		"claude-opus-4":   0.50,
		"claude-sonnet-4": 0.10,
		"gpt-4o-mini":     0.005,
	}
	val, name := pickHeadlineCost(costs, "")
	if name != "claude-opus-4" || val != 0.50 {
		t.Errorf("pickHeadlineCost(default) = (%.2f, %q), want (0.50, claude-opus-4)", val, name)
	}
}

func TestPickHeadlineCost_AllZeros(t *testing.T) {
	costs := map[string]float64{"a": 0, "b": 0, "c": 0}
	val, name := pickHeadlineCost(costs, "")
	if val != 0 {
		t.Errorf("expected 0 cost for all-zero map, got %.4f", val)
	}
	if name == "" || name == "n/a" {
		t.Errorf("expected a deterministic model name for all-zero map, got %q", name)
	}
}

func TestPickHeadlineCost_Empty(t *testing.T) {
	val, name := pickHeadlineCost(nil, "")
	if val != 0 || name != "n/a" {
		t.Errorf("pickHeadlineCost(nil) = (%.2f, %q), want (0, n/a)", val, name)
	}
}

func TestFormatUSD(t *testing.T) {
	cases := []struct {
		in   float64
		want string
	}{
		{0, "$0.0000"},
		{0.00125, "$0.0013"},
		{0.99, "$0.9900"},
		{1.00, "$1.00"},
		{167.808, "$167.81"},
	}
	for _, c := range cases {
		got := formatUSD(c.in)
		if got != c.want {
			t.Errorf("formatUSD(%.4f) = %q, want %q", c.in, got, c.want)
		}
	}
}

func TestHumanInt(t *testing.T) {
	cases := []struct {
		in   int64
		want string
	}{
		{0, "0"},
		{42, "42"},
		{999, "999"},
		{1000, "1,000"},
		{12345, "12,345"},
		{1234567, "1,234,567"},
		{-1234, "-1,234"},
	}
	for _, c := range cases {
		got := humanInt(c.in)
		if got != c.want {
			t.Errorf("humanInt(%d) = %q, want %q", c.in, got, c.want)
		}
	}
}

// TestEmitSavingsDashboard_RendersThreeBuckets is a smoke test for the
// dashboard layout: it captures stdout and asserts each bucket label and a
// representative bar cell show up, so silent regressions in the renderer
// (e.g. a missing bucket, no bar cells written) get caught.
func TestEmitSavingsDashboard_RendersThreeBuckets(t *testing.T) {
	now := time.Now().UTC()
	snap := savings.File{
		FirstSeen:   now.Add(-72 * time.Hour),
		LastUpdated: now,
		Totals:      savings.Totals{TokensSaved: 1_000_000, TokensReturned: 200_000, CallsCounted: 50},
		PerRepo: map[string]*savings.Totals{
			"/repo-a": {TokensSaved: 600_000, TokensReturned: 100_000, CallsCounted: 30},
			"/repo-b": {TokensSaved: 400_000, TokensReturned: 100_000, CallsCounted: 20},
		},
	}
	buckets := []savings.Bucket{
		{Label: "Today", Totals: savings.Totals{TokensSaved: 5_000, TokensReturned: 1_000, CallsCounted: 4},
			PerTool: []savings.ToolTotal{
				{Tool: "get_symbol_source", Totals: savings.Totals{TokensSaved: 4_000, TokensReturned: 800, CallsCounted: 3}},
				{Tool: "smart_context", Totals: savings.Totals{TokensSaved: 1_000, TokensReturned: 200, CallsCounted: 1}},
			}},
		{Label: "Last 7 days", Totals: savings.Totals{TokensSaved: 50_000, TokensReturned: 10_000, CallsCounted: 12}},
		{Label: "All time", Totals: snap.Totals},
	}

	out := captureStdout(t, func() {
		// Force a known cell width and verbose for both code paths.
		oldVerbose := savingsVerbose
		oldCells := savingsBarCells
		savingsVerbose = true
		savingsBarCells = 16
		defer func() {
			savingsVerbose = oldVerbose
			savingsBarCells = oldCells
		}()
		emitSavingsDashboard(snap, buckets, "/tmp/savings.json", "/tmp/savings.jsonl")
	})

	for _, want := range []string{
		"Gortex Token Savings",
		"Cost avoided:",
		"Today",
		"Last 7 days",
		"All time",
		"█", // at least one filled bar cell
		"░", // at least one empty bar cell
		"get_symbol_source", // verbose per-tool table
		"smart_context",
		"Cost avoided per model (all time):",
		"Per-repo totals (all time):",
		"/repo-a",
	} {
		if !strings.Contains(out, want) {
			t.Errorf("dashboard output missing %q\n----\n%s\n----", want, out)
		}
	}
}

func TestEmitSavingsDashboard_EmptyTotals(t *testing.T) {
	snap := savings.File{}
	buckets := []savings.Bucket{
		{Label: "Today"},
		{Label: "Last 7 days"},
		{Label: "All time"},
	}
	out := captureStdout(t, func() {
		emitSavingsDashboard(snap, buckets, "/tmp/savings.json", "")
	})
	if !strings.Contains(out, "No source-reading tool calls recorded yet") {
		t.Errorf("empty totals should show the no-data hint, got:\n%s", out)
	}
	// Should not print any bucket rows when there's no data.
	if strings.Contains(out, "█") {
		t.Errorf("empty totals should not render bars, got:\n%s", out)
	}
}
