package sundial

import (
	"math"
	"testing"
	"time"

	"b612.me/astro/sun"
)

func TestTrueSolarTimeMatchesSunPackage(t *testing.T) {
	date := time.Date(2026, 6, 21, 15, 30, 45, 123000000, time.FixedZone("CST", 8*3600))
	lon := 116.3913

	got := TrueSolarTime(date, lon)
	want := sun.ApparentSolarTime(date, lon)
	if !got.Equal(want) {
		t.Fatalf("true solar time mismatch: got %s want %s", got, want)
	}
}

func TestMeanSolarTimeMatchesSyntheticLongitudeZone(t *testing.T) {
	date := time.Date(2026, 6, 21, 15, 30, 45, 123000000, time.FixedZone("CST", 8*3600))
	lon := 116.3913

	got := MeanSolarTime(date, lon)
	want := date.In(time.FixedZone("LTZ", int(lon*3600.0/15.0)))
	if math.Abs(got.Sub(want).Seconds()) > 1 {
		t.Fatalf("mean solar time mismatch: got %s want %s", got, want)
	}
}

func TestEquationTimeMatchesTrueMinusMeanSolarTime(t *testing.T) {
	date := time.Date(2026, 6, 21, 15, 30, 45, 123000000, time.FixedZone("CST", 8*3600))
	lon := 116.3913

	apparent := clockHours(TrueSolarTime(date, lon))
	mean := clockHours(MeanSolarTime(date, lon))
	diff := apparent - mean
	want := sun.EquationTime(date)
	if math.Abs(diff-want) > 1e-9 {
		t.Fatalf("equation-time mismatch: got %.12f want %.12f", diff, want)
	}
}

func TestHourAngleMatchesTrueSolarClockFace(t *testing.T) {
	date := time.Date(2026, 6, 21, 15, 30, 45, 123000000, time.FixedZone("CST", 8*3600))
	lon := 116.3913

	hourAngle := HourAngle(date, lon)
	trueSolar := TrueSolarTime(date, lon)
	trueHours := float64(trueSolar.Hour()) +
		float64(trueSolar.Minute())/60 +
		float64(trueSolar.Second())/3600 +
		float64(trueSolar.Nanosecond())/3.6e12
	want := normalizeSigned180((trueHours - 12) * 15)

	if math.Abs(hourAngle-want) > 0.02 {
		t.Fatalf("hour angle mismatch: got %.6f want %.6f", hourAngle, want)
	}
}

func TestMeanSolarHourAngleMatchesInstantChain(t *testing.T) {
	date := time.Date(2026, 6, 21, 15, 30, 45, 123000000, time.FixedZone("CST", 8*3600))
	lon := 116.3913

	meanDate := MeanSolarTime(date, lon)
	meanSolarHours := clockHours(meanDate)
	got := MeanSolarHourAngle(meanDate, meanSolarHours)
	want := HourAngle(meanDate, longitudeFromTimeZone(meanDate))
	if math.Abs(got-want) > 1e-9 {
		t.Fatalf("mean-solar hour angle mismatch: got %.12f want %.12f", got, want)
	}
}

func TestZoneTimeHourAngleRebuildsTargetClockInstant(t *testing.T) {
	date := time.Date(2026, 6, 21, 15, 30, 45, 123000000, time.FixedZone("CST", 8*3600))
	lon := 116.3913
	zoneTimeHours := 9.5

	got := ZoneTimeHourAngle(date, lon, zoneTimeHours)
	want := HourAngle(dateWithClockHours(date, zoneTimeHours), lon)
	if math.Abs(got-want) > 1e-7 {
		t.Fatalf("zone-time hour angle mismatch: got %.12f want %.12f", got, want)
	}
}

func TestHorizontalHourLineAngleKnownValues(t *testing.T) {
	if math.Abs(HorizontalHourLineAngle(0, -75)) > 1e-12 {
		t.Fatalf("equatorial horizontal sundial should keep all hour lines on the noon line")
	}

	got := HorizontalHourLineAngle(45, 45)
	want := math.Atan(math.Sin(45*math.Pi/180)*math.Tan(45*math.Pi/180)) * 180 / math.Pi
	if math.Abs(got-want) > 1e-12 {
		t.Fatalf("known-value mismatch: got %.12f want %.12f", got, want)
	}

	if math.Abs(HorizontalHourLineAngle(45, -45)+got) > 1e-12 {
		t.Fatalf("morning/afternoon symmetry mismatch")
	}
}

func TestHorizontalHourLineAngleAtMatchesHourAngleChain(t *testing.T) {
	date := time.Date(2026, 6, 21, 15, 30, 45, 123000000, time.FixedZone("CST", 8*3600))
	lon := 116.3913
	lat := 39.9042

	got := HorizontalHourLineAngleAt(date, lon, lat)
	want := HorizontalHourLineAngle(lat, HourAngle(date, lon))
	if math.Abs(got-want) > 1e-12 {
		t.Fatalf("hour-line chain mismatch: got %.12f want %.12f", got, want)
	}
}

func TestInvalidHourLineInputReturnsNaN(t *testing.T) {
	if !math.IsNaN(HorizontalHourLineAngle(math.NaN(), 30)) {
		t.Fatalf("NaN latitude should produce NaN result")
	}
	if !math.IsNaN(HorizontalHourLineAngle(30, math.Inf(1))) {
		t.Fatalf("Inf hour angle should produce NaN result")
	}
	if !math.IsNaN(MeanSolarHourAngle(time.Now(), math.NaN())) {
		t.Fatalf("NaN mean-solar time should produce NaN result")
	}
	if !math.IsNaN(ZoneTimeHourAngle(time.Now(), math.Inf(1), 12)) {
		t.Fatalf("Inf longitude should produce NaN result")
	}
}
