package basic

import (
	"testing"
	"time"

	. "b612.me/astro/tools"
)

func TestMoonTopocentricPhysicalMatchesCorrectionMethod(t *testing.T) {
	jd := TD2UT(Date2JDE(testTime(2026, 4, 28, 9, 30, 45)), true)
	observerLon := 121.4737
	observerLat := 31.2304

	got := MoonTopocentricPhysical(jd, observerLon, observerLat, 0)
	want := moonTopocentricPhysicalByCorrection(jd, observerLon, observerLat)

	assertPlanetPhaseClose(t, "MoonTopocentricPhysical.LibrationLongitude", got.LibrationLongitude, want.LibrationLongitude, 0.1)
	assertPlanetPhaseClose(t, "MoonTopocentricPhysical.LibrationLatitude", got.LibrationLatitude, want.LibrationLatitude, 0.1)
	assertPlanetPhaseClose(t, "MoonTopocentricPhysical.PositionAngle", got.PositionAngle, want.PositionAngle, 0.1)
}

func TestMoonTopocentricPhysicalSampleSweepFiniteAndInRange(t *testing.T) {
	samples := []struct {
		name        string
		jd          float64
		observerLon float64
		observerLat float64
		height      float64
	}{
		{"shanghai", TD2UT(Date2JDE(testTime(2026, 4, 28, 9, 30, 45)), true), 121.4737, 31.2304, 4},
		{"chicago", TD2UT(Date2JDE(testTime(2024, 3, 25, 7, 0, 0)), true), -87.65, 41.85, 180},
	}

	for _, sample := range samples {
		info := MoonTopocentricPhysical(sample.jd, sample.observerLon, sample.observerLat, sample.height)
		prefix := sample.name + "."

		assertFiniteRange(t, prefix+"OpticalLongitude", info.OpticalLongitude, -180, 180, false)
		assertFiniteRange(t, prefix+"OpticalLatitude", info.OpticalLatitude, -90, 90, false)
		assertFiniteRange(t, prefix+"PhysicalLongitude", info.PhysicalLongitude, -180, 180, false)
		assertFiniteRange(t, prefix+"PhysicalLatitude", info.PhysicalLatitude, -90, 90, false)
		assertFiniteRange(t, prefix+"LibrationLongitude", info.LibrationLongitude, -180, 180, false)
		assertFiniteRange(t, prefix+"LibrationLatitude", info.LibrationLatitude, -90, 90, false)
		assertFiniteRange(t, prefix+"PositionAngle", info.PositionAngle, -90, 90, false)
	}
}

func moonTopocentricPhysicalByCorrection(jd, observerLon, observerLat float64) MoonPhysicalInfo {
	geocentric := MoonPhysical(jd)
	moonRA := HMoonTrueRa(jd)
	moonDec := HMoonTrueDec(jd)
	hourAngle := StarHourAngle(TD2UT(jd, false), moonRA, observerLon, 0)
	horizontalParallax := ArcSin(6378.1366 / HMoonAway(jd))

	Q := ArcTan2(
		Cos(moonDec)*Sin(hourAngle),
		Cos(moonDec)*Sin(observerLat)-Sin(moonDec)*Cos(observerLat)*Cos(hourAngle),
	)
	z := ArcCos(Sin(moonDec)*Sin(observerLat) + Cos(moonDec)*Cos(observerLat)*Cos(hourAngle))
	piPrime := horizontalParallax * (Sin(z) + 0.0084*Sin(2*z))

	deltaL := -piPrime * Sin(Q-geocentric.PositionAngle) / Cos(geocentric.LibrationLatitude)
	deltaB := piPrime * Cos(Q-geocentric.PositionAngle)
	deltaP := deltaL*Sin(geocentric.LibrationLatitude+deltaB) - piPrime*Sin(Q)*Tan(moonDec)

	return MoonPhysicalInfo{
		OpticalLongitude:   geocentric.OpticalLongitude,
		OpticalLatitude:    geocentric.OpticalLatitude,
		PhysicalLongitude:  geocentric.PhysicalLongitude,
		PhysicalLatitude:   geocentric.PhysicalLatitude,
		LibrationLongitude: wrapSignedAngle180(geocentric.LibrationLongitude + deltaL),
		LibrationLatitude:  geocentric.LibrationLatitude + deltaB,
		PositionAngle:      geocentric.PositionAngle + deltaP,
	}
}

func testTime(year int, month time.Month, day, hour, minute, second int) time.Time {
	return time.Date(year, month, day, hour, minute, second, 0, time.UTC)
}
