package moon

import (
	"errors"
	"math"
	"testing"
	"time"

	"b612.me/astro/basic"
)

func TestFindStarOccultationsFindsPointSourceEvent(t *testing.T) {
	observer := Observer{Longitude: 121.4737, Latitude: 31.2304, Height: 4}
	eventTime := time.Date(2026, 8, 2, 12, 0, 0, 0, time.UTC)
	tt := occultationTestTimeToTT(eventTime)
	ra, dec := occultationTestMoonTopocentricRaDec(tt, observer, -1)
	star := StarCoordinate{
		ID:    "synthetic-point-source",
		RA:    ra,
		Dec:   dec,
		Epoch: eventTime,
		Frame: CoordinateFrameApparentOfDate,
	}

	start := eventTime.In(time.FixedZone("CST", 8*3600)).Add(-3 * time.Hour)
	end := eventTime.In(time.FixedZone("CST", 8*3600)).Add(3 * time.Hour)
	results, err := FindStarOccultations(start, end, star,
		observer.Longitude, observer.Latitude, observer.Height, OccultationSearchOptions{MaxEvents: 1})
	if err != nil {
		t.Fatalf("FindStarOccultations() error = %v", err)
	}
	if len(results) != 1 {
		t.Fatalf("FindStarOccultations() returned %d events, want 1", len(results))
	}
	result := results[0]
	if result.Observer != observer {
		t.Fatalf("observer = %+v, want %+v", result.Observer, observer)
	}
	if result.Greatest.Location().String() != "CST" {
		t.Fatalf("greatest location = %q, want CST", result.Greatest.Location().String())
	}
	if result.Type != OccultationTotal {
		t.Fatalf("event type = %q, want %q", result.Type, OccultationTotal)
	}
	if result.MinimumSeparationArcsec > result.MoonSemidiameterArcsec {
		t.Fatalf("minimum separation %.3f exceeds lunar radius %.3f", result.MinimumSeparationArcsec, result.MoonSemidiameterArcsec)
	}
	if result.Immersion.IsZero() || result.Emersion.IsZero() {
		t.Fatalf("contact times must be populated: immersion=%v emersion=%v", result.Immersion, result.Emersion)
	}
	if !result.ContactsComplete {
		t.Fatal("contacts must be marked complete")
	}
	if !(result.Immersion.Before(result.Greatest) && result.Greatest.Before(result.Emersion)) {
		t.Fatalf("contact ordering invalid: immersion=%v greatest=%v emersion=%v", result.Immersion, result.Greatest, result.Emersion)
	}
	if delta := math.Abs(result.Greatest.Sub(eventTime).Seconds()); delta > 60 {
		t.Fatalf("greatest differs from synthetic event by %.1fs", delta)
	}
	if result.MoonAzimuthAtGreatest < 0 || result.MoonAzimuthAtGreatest >= 360 {
		t.Fatalf("moon azimuth = %.6f, want [0,360)", result.MoonAzimuthAtGreatest)
	}
	if result.PositionAngleDeg < 0 || result.PositionAngleDeg >= 360 {
		t.Fatalf("position angle = %.6f, want [0,360)", result.PositionAngleDeg)
	}
}

func TestFindBestStarOccultationsReturnsTopocentricGroundPoint(t *testing.T) {
	observer := Observer{Longitude: 121.4737, Latitude: 31.2304, Height: 4}
	eventTime := time.Date(2026, 8, 2, 12, 0, 0, 0, time.UTC)
	ra, dec := occultationTestMoonTopocentricRaDec(occultationTestTimeToTT(eventTime), observer, -1)
	star := StarCoordinate{
		ID:    "synthetic-best-point-source",
		RA:    ra,
		Dec:   dec,
		Epoch: eventTime,
		Frame: CoordinateFrameApparentOfDate,
	}

	results, err := FindBestStarOccultations(
		eventTime.Add(-3*time.Hour), eventTime.Add(3*time.Hour), star, OccultationSearchOptions{MaxEvents: 1})
	if err != nil {
		t.Fatalf("FindBestStarOccultations() error = %v", err)
	}
	if len(results) != 1 {
		t.Fatalf("FindBestStarOccultations() returned %d events, want 1", len(results))
	}
	result := results[0]
	if result.Observer.Longitude < -180 || result.Observer.Longitude > 180 || result.Observer.Latitude < -90 || result.Observer.Latitude > 90 {
		t.Fatalf("best observer out of bounds: %+v", result.Observer)
	}
	if result.Observer.Height != 0 {
		t.Fatalf("best observer height = %.1f, want sea-level default", result.Observer.Height)
	}
	if result.MinimumSeparationArcsec > result.MoonSemidiameterArcsec {
		t.Fatalf("best point is not an occultation: separation=%.3f radius=%.3f", result.MinimumSeparationArcsec, result.MoonSemidiameterArcsec)
	}
	if !result.VisibleAtGreatest || result.MoonAltitudeAtGreatest < 0 {
		t.Fatalf("best observer must see the Moon: altitude=%.6f visible=%v", result.MoonAltitudeAtGreatest, result.VisibleAtGreatest)
	}
	if result.Immersion.IsZero() || result.Emersion.IsZero() || !result.Immersion.Before(result.Greatest) || !result.Greatest.Before(result.Emersion) {
		t.Fatalf("best point contact ordering invalid: immersion=%v greatest=%v emersion=%v", result.Immersion, result.Greatest, result.Emersion)
	}
}

func TestFindStarOccultationsLatitudePrefilterRejectsPolarStar(t *testing.T) {
	star := StarCoordinate{
		ID:    "polar-star",
		RA:    0,
		Dec:   89,
		Epoch: time.Date(2000, 1, 1, 12, 0, 0, 0, time.UTC),
		Frame: CoordinateFrameICRS,
	}
	results, err := FindStarOccultations(
		time.Date(2026, 8, 1, 0, 0, 0, 0, time.UTC),
		time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC),
		star, 0, 0, 0, OccultationSearchOptions{})
	if err != nil {
		t.Fatalf("FindStarOccultations() error = %v", err)
	}
	if len(results) != 0 {
		t.Fatalf("polar star returned %d events, want none", len(results))
	}
}

func TestFindStarOccultationsReturnsCompleteContactsOutsideQueryWindow(t *testing.T) {
	observer := Observer{Longitude: 121.4737, Latitude: 31.2304, Height: 4}
	eventTime := time.Date(2026, 8, 2, 12, 0, 0, 0, time.UTC)
	ra, dec := occultationTestMoonTopocentricRaDec(occultationTestTimeToTT(eventTime), observer, -1)
	star := StarCoordinate{ID: "clipped-window", RA: ra, Dec: dec, Epoch: eventTime, Frame: CoordinateFrameApparentOfDate}
	start := eventTime.Add(-time.Minute)
	end := eventTime.Add(time.Minute)

	results, err := FindStarOccultations(start, end, star,
		observer.Longitude, observer.Latitude, observer.Height, OccultationSearchOptions{})
	if err != nil {
		t.Fatalf("FindStarOccultations() error = %v", err)
	}
	if len(results) != 1 {
		t.Fatalf("FindStarOccultations() returned %d events, want 1", len(results))
	}
	result := results[0]
	if !result.ContactsComplete || result.Immersion.IsZero() || result.Emersion.IsZero() {
		t.Fatalf("incomplete contacts: %+v", result)
	}
	if !result.Immersion.Before(start) || !result.Emersion.After(end) {
		t.Fatalf("contacts were clipped to query window: immersion=%v window=%v..%v emersion=%v", result.Immersion, start, end, result.Emersion)
	}
}

func occultationTestTimeToTT(value time.Time) float64 {
	return basic.TD2UT(basic.Date2JDE(value.UTC()), true)
}

func occultationTestMoonTopocentricRaDec(tt float64, observer Observer, n int) (float64, float64) {
	ra, dec := basic.HMoonGeocentricApparentRaDecN(tt, n)
	distanceAU := basic.HMoonAwayN(tt, n) / 149597870.7
	ra, dec = basic.TopocentricRaDec(ra, dec, observer.Latitude, observer.Longitude, basic.TD2UT(tt, false), distanceAU, observer.Height)
	ra = math.Mod(ra, 360)
	if ra < 0 {
		ra += 360
	}
	return ra, dec
}

func hr4799CoordinateForTest() StarCoordinate {
	return StarCoordinate{
		ID:                             "HR 4799",
		RA:                             189.1975,
		Dec:                            -5.831944444444,
		Epoch:                          time.Date(2000, 1, 1, 12, 0, 0, 0, time.UTC),
		Frame:                          CoordinateFrameJ2000,
		ProperMotionRACosDecMasPerYear: -28,
		ProperMotionDecMasPerYear:      -18,
	}
}

func TestFindStarOccultationsValidatesInputs(t *testing.T) {
	star := StarCoordinate{
		RA:    10,
		Dec:   20,
		Epoch: time.Date(2000, 1, 1, 12, 0, 0, 0, time.UTC),
		Frame: CoordinateFrameICRS,
	}
	_, err := FindStarOccultations(time.Time{}, time.Time{}, star, 0, 0, 0, OccultationSearchOptions{})
	if !errors.Is(err, ErrInvalidOccultationInput) {
		t.Fatalf("FindStarOccultations() error = %v, want ErrInvalidOccultationInput", err)
	}
	start := time.Date(2026, 8, 2, 0, 0, 0, 0, time.UTC)
	_, err = FindStarOccultations(start, start, star, 0, 0, 0, OccultationSearchOptions{})
	if !errors.Is(err, ErrInvalidOccultationInput) {
		t.Fatalf("FindStarOccultations() unordered range error = %v, want ErrInvalidOccultationInput", err)
	}
	_, err = FindStarOccultations(start, start.Add(time.Hour), star, math.NaN(), 0, 0, OccultationSearchOptions{})
	if !errors.Is(err, ErrInvalidOccultationInput) {
		t.Fatalf("FindStarOccultations() observer error = %v, want ErrInvalidOccultationInput", err)
	}
}
