package runner

import (
	"context"
	"errors"
	"math"
	"testing"

	"github.com/easyai/easyai-ai-gateway/apps/api/internal/clients"
	"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)

func TestFixedAmountPreservesNineDecimalPlaces(t *testing.T) {
	amount, err := parseFixedAmount("0.123456789")
	if err != nil {
		t.Fatalf("parse fixed amount: %v", err)
	}
	if got, want := amount.String(), "0.123456789"; got != want {
		t.Fatalf("amount.String()=%q, want %q", got, want)
	}

	price := mustFixedAmount(t, "0.000000001")
	product, err := multiplyFixedAmountByInt(price, 3)
	if err != nil {
		t.Fatalf("multiply fixed amount: %v", err)
	}
	if got, want := product.String(), "0.000000003"; got != want {
		t.Fatalf("nano amount multiplication=%q, want %q", got, want)
	}
}

func TestFixedAmountOperationsRejectOverflow(t *testing.T) {
	maximum := fixedAmount(math.MaxInt64)
	if _, err := multiplyFixedAmountByInt(maximum, 2); !errors.Is(err, errFixedAmountOverflow) {
		t.Fatalf("multiply overflow error=%v", err)
	}
	if _, err := addFixedAmounts(maximum, 1); !errors.Is(err, errFixedAmountOverflow) {
		t.Fatalf("add overflow error=%v", err)
	}
	pricing := resolvedPricing{RuleSetID: "overflow-rule"}
	if _, err := pricing.calculate("image", maximum, []int{2}); !isPricingUnavailable(err) {
		t.Fatalf("pricing overflow should be unavailable: %v", err)
	}
	if _, err := multiplyFixedProductRatio(maximum, []int{2}, nil, 1); !errors.Is(err, errFixedAmountOverflow) {
		t.Fatalf("product ratio overflow error=%v", err)
	}
}

func TestEstimatedOutputTokensUsesAliasesAndCapabilityFallback(t *testing.T) {
	candidate := store.RuntimeModelCandidate{
		ModelType: "text_generate",
		Capabilities: map[string]any{
			"text_generate": map[string]any{"max_output_tokens": 8192},
		},
	}
	tests := []struct {
		name string
		body map[string]any
		want int
	}{
		{name: "max completion has highest priority", body: map[string]any{"max_completion_tokens": 700, "max_output_tokens": 600, "max_tokens": 500}, want: 700},
		{name: "max output precedes legacy max tokens", body: map[string]any{"max_output_tokens": 600, "max_tokens": 500}, want: 600},
		{name: "legacy max tokens", body: map[string]any{"max_tokens": 500}, want: 500},
		{name: "model capability fallback", body: map[string]any{}, want: 8192},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			if got := estimatedOutputTokens(test.body, candidate); got != test.want {
				t.Fatalf("estimatedOutputTokens()=%d, want %d", got, test.want)
			}
		})
	}

	if got := estimatedOutputTokens(nil, store.RuntimeModelCandidate{}); got != 4096 {
		t.Fatalf("missing capability fallback=%d, want 4096", got)
	}
}

func TestBuildEstimateResultUsesPreferredTotalAndMaximumReservation(t *testing.T) {
	result, err := buildEstimateResult([]candidateEstimate{
		{Items: []any{map[string]any{"amount": 1.25, "currency": "resource"}}, Amount: mustFixedAmount(t, "1.25")},
		{Items: []any{map[string]any{"amount": 2.75, "currency": "resource"}}, Amount: mustFixedAmount(t, "2.75")},
	}, "fingerprint")
	if err != nil {
		t.Fatalf("build estimate result: %v", err)
	}
	if result.TotalAmount != 1.25 {
		t.Fatalf("preferred total=%v, want 1.25", result.TotalAmount)
	}
	if result.ReservationAmount != 2.75 {
		t.Fatalf("reservation=%v, want 2.75", result.ReservationAmount)
	}
	if result.CandidateCount != 2 || result.PricingVersion != pricingVersionV2 || result.RequestFingerprint != "fingerprint" {
		t.Fatalf("unexpected estimate metadata: %+v", result)
	}
}

func TestPricingAvailabilityRequiresExplicitFree(t *testing.T) {
	paid := resolvedPricing{Config: map[string]any{"imageBase": 1.5}, Currency: "resource"}
	if _, err := paid.requiredPrice("image", "imageBase", "basePrice"); err != nil {
		t.Fatalf("positive price should be available: %v", err)
	}

	missing := resolvedPricing{Config: map[string]any{}, Currency: "resource"}
	if _, err := missing.requiredPrice("image", "imageBase", "basePrice"); !isPricingUnavailable(err) {
		t.Fatalf("missing price should be unavailable, got %v", err)
	}

	ordinaryZero := resolvedPricing{Config: map[string]any{"imageBase": 0}, Currency: "resource"}
	if _, err := ordinaryZero.requiredPrice("image", "imageBase", "basePrice"); !isPricingUnavailable(err) {
		t.Fatalf("ordinary zero should be unavailable, got %v", err)
	}

	explicitFree := resolvedPricing{
		Config:       map[string]any{"imageBase": 0},
		Currency:     "resource",
		FreeResource: map[string]bool{"image": true},
	}
	price, err := explicitFree.requiredPrice("image", "imageBase", "basePrice")
	if err != nil || !price.IsZero() {
		t.Fatalf("explicit free should resolve to zero: price=%s err=%v", price.String(), err)
	}
}

func TestPricingWeightsUseFixedPrecisionAndRejectInvalidValues(t *testing.T) {
	pricing := resolvedPricing{Config: map[string]any{
		"image": map[string]any{
			"dynamicWeight": map[string]any{
				"qualityFactors": map[string]any{"high": "1.123456789", "broken": 0},
			},
		},
	}}
	weight, err := pricing.weight("image", "qualityWeights", "high")
	if err != nil || weight.String() != "1.123456789" {
		t.Fatalf("exact weight=%s err=%v", weight.String(), err)
	}
	if _, err := pricing.weight("image", "qualityWeights", "broken"); !isPricingUnavailable(err) {
		t.Fatalf("invalid configured weight should make pricing unavailable: %v", err)
	}
	defaultWeight, err := pricing.weight("image", "qualityWeights", "unconfigured")
	if err != nil || defaultWeight.String() != "1.000000000" {
		t.Fatalf("default weight=%s err=%v", defaultWeight.String(), err)
	}
}

func TestVideoBillingV2ProratesFiveSecondPriceByActualDuration(t *testing.T) {
	service := &Service{}
	candidate := store.RuntimeModelCandidate{ModelName: "video-model"}
	pricing := resolvedPricing{
		Config: map[string]any{
			"video": map[string]any{
				"basePrice": 100,
				"dynamicWeight": map[string]any{
					"audioWeights": map[string]any{"true": 2},
				},
			},
		},
		Currency: "resource",
	}

	tests := []struct {
		name       string
		duration   float64
		wantUnits  float64
		wantAmount float64
	}{
		{name: "three seconds uses zero point six units", duration: 3, wantUnits: 0.6, wantAmount: 120},
		{name: "five seconds uses one unit", duration: 5, wantUnits: 1, wantAmount: 200},
		{name: "six seconds uses one point two units", duration: 6, wantUnits: 1.2, wantAmount: 240},
		{name: "fractional seconds retain fixed amount precision", duration: 6.000000001, wantUnits: 1.2000000002, wantAmount: 240.00000004},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			items, total, _, err := service.billingsWithResolvedPricingV2(
				context.Background(), nil, "videos.generations",
				map[string]any{"duration": test.duration, "audio": true},
				candidate, clients.Response{}, true, pricing,
			)
			if err != nil {
				t.Fatalf("bill video: %v", err)
			}
			line := firstBillingLine(t, items)
			if got := total.Float64(); math.Abs(got-test.wantAmount) > 1e-9 {
				t.Fatalf("total amount=%v, want %v", got, test.wantAmount)
			}
			if got := floatFromAny(line["amount"]); math.Abs(got-test.wantAmount) > 1e-9 {
				t.Fatalf("line amount=%v, want %v", got, test.wantAmount)
			}
			if got := floatFromAny(line["quantity"]); math.Abs(got-test.wantUnits) > 1e-12 {
				t.Fatalf("quantity=%v, want %v", got, test.wantUnits)
			}
			if got := floatFromAny(line["durationUnitCount"]); math.Abs(got-test.wantUnits) > 1e-12 {
				t.Fatalf("duration units=%v, want %v", got, test.wantUnits)
			}
		})
	}
}

func TestVideoBillingV2RoundsOnlyAfterApplyingDurationCountAndWeights(t *testing.T) {
	service := &Service{}
	candidate := store.RuntimeModelCandidate{ModelName: "video-model"}

	tests := []struct {
		name       string
		body       map[string]any
		pricing    resolvedPricing
		wantAmount string
	}{
		{
			name: "count preserves a sub-nano duration share",
			body: map[string]any{"duration": 1, "count": 5},
			pricing: resolvedPricing{Config: map[string]any{
				"video": map[string]any{"basePrice": "0.000000001"},
			}},
			wantAmount: "0.000000001",
		},
		{
			name: "weight does not amplify a rounded duration share",
			body: map[string]any{"duration": 3, "audio": true},
			pricing: resolvedPricing{Config: map[string]any{
				"video": map[string]any{
					"basePrice": "0.000000001",
					"dynamicWeight": map[string]any{
						"audioWeights": map[string]any{"true": 2},
					},
				},
			}},
			wantAmount: "0.000000001",
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			_, total, _, err := service.billingsWithResolvedPricingV2(
				context.Background(), nil, "videos.generations", test.body,
				candidate, clients.Response{}, true, test.pricing,
			)
			if err != nil {
				t.Fatalf("bill video: %v", err)
			}
			if got := total.String(); got != test.wantAmount {
				t.Fatalf("total amount=%s, want %s", got, test.wantAmount)
			}
		})
	}
}

func mustFixedAmount(t *testing.T, value string) fixedAmount {
	t.Helper()
	amount, err := parseFixedAmount(value)
	if err != nil {
		t.Fatalf("parse %q: %v", value, err)
	}
	return amount
}
