package models

import (
	"math"
	"testing"
	"time"
)

// The charging figures are derived from the whole history on every read, so the
// window rules are the thing worth pinning down: what closes a window, what
// folds into it, and what makes one uncomputable.

func day(n int) time.Time { return time.Date(2026, 3, n, 8, 0, 0, 0, time.UTC) }

func TestComputeChargingDerivedWindows(t *testing.T) {
	// A first full charge (no window before it), then 300 km on 45 kWh split
	// across a partial top-up and the full charge that closes the window.
	entries := []ChargingSession{
		{Date: day(1), Km: 10000, Kwh: 50, Cost: 100, FullCharge: true},
		{Date: day(3), Km: 10120, Kwh: 15, Cost: 45},
		{Date: day(6), Km: 10300, Kwh: 30, Cost: 75, FullCharge: true},
	}
	ComputeChargingDerived(entries)

	// The first full charge has nothing before it to measure against.
	if entries[0].ConsumptionKwh100 != nil {
		t.Errorf("first full charge got a consumption figure: %v", *entries[0].ConsumptionKwh100)
	}
	// A partial charge never closes a window — it folds into the next full one.
	if entries[1].ConsumptionKwh100 != nil {
		t.Errorf("partial charge got a consumption figure: %v", *entries[1].ConsumptionKwh100)
	}

	closing := entries[2]
	if closing.DistanceKm == nil || *closing.DistanceKm != 300 {
		t.Fatalf("distance = %v, want 300", closing.DistanceKm)
	}
	// 15 + 30 kWh over 300 km: the partial counts, the opening charge does not.
	if closing.KwhUsed == nil || *closing.KwhUsed != 45 {
		t.Fatalf("kwhUsed = %v, want 45", closing.KwhUsed)
	}
	assertFloat(t, "consumption", closing.ConsumptionKwh100, 15)  // 45/300*100
	assertFloat(t, "kmPerKwh", closing.KmPerKwh, 300.0/45.0)      //  6.67
	assertFloat(t, "costPerKm", closing.CostPerKm, (45+75)/300.0) //  0.40
	assertFloat(t, "pricePerKwh", closing.PricePerKwh, 75.0/30.0) //  2.50
}

func TestComputeChargingDerivedMissedSession(t *testing.T) {
	// The car was charged somewhere without being logged, so the kWh on record
	// do not account for the distance: reporting a figure would be fiction.
	entries := []ChargingSession{
		{Date: day(1), Km: 10000, Kwh: 50, Cost: 100, FullCharge: true},
		{Date: day(5), Km: 10400, Kwh: 40, Cost: 80, FullCharge: true, MissedSession: true},
	}
	ComputeChargingDerived(entries)
	if entries[1].ConsumptionKwh100 != nil {
		t.Errorf("window with a missed session got a figure: %v", *entries[1].ConsumptionKwh100)
	}
	// The price of that charge is still known — it is on the receipt.
	assertFloat(t, "pricePerKwh", entries[1].PricePerKwh, 2)
}

func TestComputeChargingStats(t *testing.T) {
	// Two computable windows of different lengths: 100 km at 20 kWh/100km and
	// 300 km at 10 kWh/100km. The average has to be distance-weighted (40 kWh
	// over 400 km = 10 kWh/100km), not the mean of the two figures (15).
	entries := []ChargingSession{
		{Date: day(1), Km: 10000, Kwh: 10, Cost: 20, FullCharge: true},
		{Date: day(2), Km: 10100, Kwh: 20, Cost: 40, FullCharge: true},
		{Date: day(4), Km: 10400, Kwh: 30, Cost: 60, FullCharge: true},
	}
	ComputeChargingDerived(entries)
	st := ComputeChargingStats(entries)

	if st.Entries != 3 || st.TotalKwh != 60 || st.TotalCost != 120 {
		t.Fatalf("totals = %d entries, %v kWh, %v cost", st.Entries, st.TotalKwh, st.TotalCost)
	}
	if st.TrackedDistanceKm != 400 {
		t.Fatalf("trackedDistance = %d, want 400", st.TrackedDistanceKm)
	}
	assertFloat(t, "avg", st.AvgConsumptionKwh100, 12.5) // 50 kWh over 400 km
	assertFloat(t, "best", st.BestConsumptionKwh100, 10) // the 300 km window
	assertFloat(t, "worst", st.WorstConsumptionKwh100, 20)
	assertFloat(t, "avgPricePerKwh", st.AvgPricePerKwh, 2)
	assertFloat(t, "costPerKm", st.CostPerKm, 0.25) // 100 spent over 400 km
}

func TestComputeChargingStatsEmpty(t *testing.T) {
	st := ComputeChargingStats(nil)
	if st.Entries != 0 || st.AvgConsumptionKwh100 != nil || st.FirstDate != nil {
		t.Errorf("empty history summarised as %+v", st)
	}
}

func assertFloat(t *testing.T, name string, got *float64, want float64) {
	t.Helper()
	if got == nil {
		t.Fatalf("%s = nil, want %v", name, want)
	}
	if math.Abs(*got-want) > 1e-9 {
		t.Fatalf("%s = %v, want %v", name, *got, want)
	}
}
