package ledger

import (
	"math"
	"testing"

	"textmachine/backend/internal/llm"
)

func approx(t *testing.T, got, want float64, msg string) {
	t.Helper()
	if math.Abs(got-want) > 1e-12 {
		t.Fatalf("%s: got %v, want %v", msg, got, want)
	}
}

// Формула денег — ядро юнит-экономики; проверяем каждое слагаемое, включая
// новое (cache write), и деградацию к формуле vojo без кэш-полей.
func TestCostUSD(t *testing.T) {
	price := ModelPrice{InputPerM: 2.0, CachedPerM: 0.2, CacheWritePerM: 2.5, OutputPerM: 10.0}

	// Anthropic-подобный вызов: 10k всего входа = 6k некэш + 3k cache-read +
	// 1k cache-write; 2k выхода.
	u := llm.Usage{PromptTokens: 10_000, CachedTokens: 3_000, CacheCreationTokens: 1_000, CompletionTokens: 2_000}
	want := 6_000*2.0/1e6 + 3_000*0.2/1e6 + 1_000*2.5/1e6 + 2_000*10.0/1e6
	approx(t, CostUSD(price, u), want, "anthropic-style usage")

	// OpenAI-совместимый вызов без кэш-полей — формула vojo.
	u2 := llm.Usage{PromptTokens: 10_000, CompletionTokens: 2_000}
	approx(t, CostUSD(price, u2), 10_000*2.0/1e6+2_000*10.0/1e6, "plain usage")

	// Reasoning-токены биллятся по output-ставке ПОВЕРХ completion (xAI).
	u3 := llm.Usage{PromptTokens: 1_000, CompletionTokens: 500, ReasoningTokens: 1_500}
	approx(t, CostUSD(price, u3), 1_000*2.0/1e6+2_000*10.0/1e6, "additive reasoning")

	// Защита от провайдера, отрапортовавшего cached > prompt: некэш-слагаемое
	// зажимается в 0, а не уходит в минус.
	u4 := llm.Usage{PromptTokens: 100, CachedTokens: 500, CompletionTokens: 10}
	want4 := 0*2.0/1e6 + 500*0.2/1e6 + 10*10.0/1e6
	approx(t, CostUSD(price, u4), want4, "cached > prompt guard")

	// Отрицательные usage-числа (баг учёта провайдера) не должны УМЕНЬШАТЬ
	// стоимость — каждое слагаемое зажимается в ≥0.
	u5 := llm.Usage{PromptTokens: -1000, CachedTokens: -50, CompletionTokens: -200, ReasoningTokens: -5}
	if got := CostUSD(price, u5); got != 0 {
		t.Fatalf("negative usage must never reduce cost below 0, got %v", got)
	}
}

func TestPriceForResponse(t *testing.T) {
	def := ModelPrice{InputPerM: 0.14, OutputPerM: 0.28} // дешёвый якорь
	p, err := NewPricer(map[string]ModelPrice{
		"deepseek-v4-flash": def,
		"claude-sonnet-5":   {InputPerM: 2.0, OutputPerM: 10.0},
	}, def)
	if err != nil {
		t.Fatal(err)
	}
	// Фактически ответившая известна — по ней.
	if p.PriceForResponse("claude-sonnet-5", "claude-sonnet-5").InputPerM != 2.0 {
		t.Fatal("known actual model must price by actual")
	}
	// Провайдер вернул датированный слаг премиум-модели — НЕ дешёвый якорь, а
	// цена ЗАПРОШЕННОЙ модели (иначе систематический недоучёт премиума).
	if got := p.PriceForResponse("claude-sonnet-5", "claude-sonnet-5-20260101").InputPerM; got != 2.0 {
		t.Fatalf("unknown actual must fall back to requested model price (2.0), got %v", got)
	}
	// Обе неизвестны — дефолтный якорь (never $0).
	if got := p.PriceForResponse("mystery", "also-mystery").InputPerM; got != 0.14 {
		t.Fatalf("both unknown must fall back to default anchor, got %v", got)
	}
}

func TestPricerNeverZero(t *testing.T) {
	def := ModelPrice{InputPerM: 1, OutputPerM: 2}
	p, err := NewPricer(map[string]ModelPrice{"known": {InputPerM: 5, CachedPerM: 1, OutputPerM: 10}}, def)
	if err != nil {
		t.Fatal(err)
	}
	if got := p.PriceFor("known").InputPerM; got != 5 {
		t.Fatalf("known model price: got %v", got)
	}
	// Неизвестная модель (фактическая после фейловера) — дефолтный якорь, не $0.
	if got := p.PriceFor("mystery-model").InputPerM; got != 1 {
		t.Fatalf("unknown model must fall back to default price, got %v", got)
	}
	// $0-дефолт ослепил бы потолки — конструктор обязан отказать.
	if _, err := NewPricer(nil, ModelPrice{}); err == nil {
		t.Fatal("zero default price must be rejected")
	}
}

func TestEstimateUSDIsPessimistic(t *testing.T) {
	price := ModelPrice{InputPerM: 2.0, CachedPerM: 0.2, OutputPerM: 10.0}
	est := EstimateUSD(price, 1_000, 2_000, 0)
	approx(t, est, 1_000*2.0/1e6+2_000*10.0/1e6, "estimate")
	// Реальный вызов с кэш-хитом обязан стоить не больше оценки — иначе
	// settle раздует committed выше потолка, пропущенного на допуске.
	real := CostUSD(price, llm.Usage{PromptTokens: 1_000, CachedTokens: 900, CompletionTokens: 2_000})
	if real > est {
		t.Fatalf("real cost %v exceeds reservation estimate %v", real, est)
	}
}

// TestEstimateUSDAdditiveReasoningBuffer covers D13.6: the additive-reasoning buffer is
// reserved as output tokens, so an xAI-style think-ON call whose reasoning lands on top of
// completion no longer overshoots the reservation and blinds the ceiling.
func TestEstimateUSDAdditiveReasoningBuffer(t *testing.T) {
	price := ModelPrice{InputPerM: 2.0, OutputPerM: 10.0}
	base := EstimateUSD(price, 1_000, 2_000, 0)
	withBuf := EstimateUSD(price, 1_000, 2_000, 3_000)
	// The buffer adds exactly 3_000 output-priced tokens over the base estimate.
	approx(t, withBuf-base, 3_000*10.0/1e6, "reasoning buffer term")
	// A real additive call (completion 2_000 + reasoning 2_500 ≤ buffer 3_000) stays within.
	real := CostUSD(price, llm.Usage{PromptTokens: 1_000, CompletionTokens: 2_000, ReasoningTokens: 2_500})
	if real > withBuf {
		t.Fatalf("additive real cost %v exceeds buffered reservation %v", real, withBuf)
	}
	if real <= base {
		t.Fatalf("without the buffer the reservation %v would be blind to the reasoning spend (real %v)", base, real)
	}
}
