package basic

import (
	"encoding/json"
	"math"
	"os"
	"testing"
	"time"
)

type moonPlanetConjunctionBaselineSample struct {
	Planet  string `json:"planet"`
	Year    int    `json:"year"`
	Month   int    `json:"month"`
	TimeUTC string `json:"time_utc"`
}

type moonPlanetConjunctionBaseline struct {
	Samples []moonPlanetConjunctionBaselineSample `json:"samples"`
}

func loadMoonPlanetConjunctionBaseline(t *testing.T) moonPlanetConjunctionBaseline {
	t.Helper()

	paths := [][]string{
		{
			"testdata/moon_planet_conjunction_baseline.json",
			"basic/testdata/moon_planet_conjunction_baseline.json",
		},
		{
			"testdata/moon_planet_conjunction_baseline_samples.json",
			"basic/testdata/moon_planet_conjunction_baseline_samples.json",
		},
	}

	var merged moonPlanetConjunctionBaseline
	for index, candidates := range paths {
		var (
			data []byte
			err  error
		)
		for _, path := range candidates {
			data, err = os.ReadFile(path)
			if err == nil {
				var baseline moonPlanetConjunctionBaseline
				if err := json.Unmarshal(data, &baseline); err != nil {
					t.Fatalf("decode baseline %s: %v", path, err)
				}
				merged.Samples = append(merged.Samples, baseline.Samples...)
				break
			}
		}
		if err != nil && index == 0 {
			t.Fatalf("read baseline: %v", err)
		}
	}
	if len(merged.Samples) == 0 {
		t.Fatal("empty moon-planet conjunction baseline")
	}
	return merged
}

func TestMoonPlanetConjunctionsMatchHorizonsBaseline(t *testing.T) {
	baseline := loadMoonPlanetConjunctionBaseline(t)

	type conjunctionCase struct {
		planet MoonPlanetConjunctionPlanet
		next   func(float64, MoonPlanetConjunctionPlanet) float64
	}

	cases := map[string]conjunctionCase{
		"mercury": {planet: MoonPlanetConjunctionMercury, next: NextMoonPlanetConjunction},
		"venus":   {planet: MoonPlanetConjunctionVenus, next: NextMoonPlanetConjunction},
		"mars":    {planet: MoonPlanetConjunctionMars, next: NextMoonPlanetConjunction},
		"jupiter": {planet: MoonPlanetConjunctionJupiter, next: NextMoonPlanetConjunction},
		"saturn":  {planet: MoonPlanetConjunctionSaturn, next: NextMoonPlanetConjunction},
		"uranus":  {planet: MoonPlanetConjunctionUranus, next: NextMoonPlanetConjunction},
		"neptune": {planet: MoonPlanetConjunctionNeptune, next: NextMoonPlanetConjunction},
	}

	const tolerance = 20 * time.Second
	var maxDiff time.Duration

	seen := make(map[string]int, len(cases))
	for _, sample := range baseline.Samples {
		tc, ok := cases[sample.Planet]
		if !ok {
			t.Fatalf("unknown planet %q", sample.Planet)
		}

		wantTime, err := time.Parse(time.RFC3339Nano, sample.TimeUTC)
		if err != nil {
			t.Fatalf("parse sample time %q: %v", sample.TimeUTC, err)
		}
		queryTT := TD2UT(Date2JDE(wantTime.Add(-12*time.Hour).UTC()), true)
		gotUT := tc.next(queryTT, tc.planet)
		gotTime := JDE2DateByZone(gotUT, time.UTC, false)
		diff := gotTime.Sub(wantTime)
		if diff < 0 {
			diff = -diff
		}
		if diff > maxDiff {
			maxDiff = diff
		}
		if diff > tolerance {
			t.Fatalf("%s %04d-%02d time mismatch: got %s want %s tolerance %v", sample.Planet, sample.Year, sample.Month, gotTime.Format(time.RFC3339Nano), sample.TimeUTC, tolerance)
		}

		delta := math.Abs(moonPlanetConjunctionDeltaAt(TD2UT(gotUT, true), tc.planet, -1))
		if delta > 0.01 {
			t.Fatalf("%s %04d-%02d event not near conjunction: delta=%.8f deg", sample.Planet, sample.Year, sample.Month, delta)
		}
		seen[sample.Planet]++
	}

	for planet := range cases {
		if seen[planet] == 0 {
			t.Fatalf("missing baseline samples for %s", planet)
		}
	}

	t.Logf("moon-planet conjunction max diff: time=%v", maxDiff)
}

func TestMoonPlanetConjunctionDirectionalConsistencyAtComputedEvent(t *testing.T) {
	baseline := loadMoonPlanetConjunctionBaseline(t)

	planets := map[string]MoonPlanetConjunctionPlanet{
		"mercury": MoonPlanetConjunctionMercury,
		"venus":   MoonPlanetConjunctionVenus,
		"mars":    MoonPlanetConjunctionMars,
		"jupiter": MoonPlanetConjunctionJupiter,
		"saturn":  MoonPlanetConjunctionSaturn,
		"uranus":  MoonPlanetConjunctionUranus,
		"neptune": MoonPlanetConjunctionNeptune,
	}

	for _, sample := range baseline.Samples {
		planet, ok := planets[sample.Planet]
		if !ok {
			t.Fatalf("unknown planet %q", sample.Planet)
		}
		wantTime, err := time.Parse(time.RFC3339Nano, sample.TimeUTC)
		if err != nil {
			t.Fatalf("parse sample time %q: %v", sample.TimeUTC, err)
		}
		seedTT := TD2UT(Date2JDE(wantTime.Add(-12*time.Hour).UTC()), true)
		eventUT := NextMoonPlanetConjunction(seedTT, planet)
		eventTime := JDE2DateByZone(eventUT, time.UTC, false)
		queryAtTT := TD2UT(Date2JDE(eventTime.UTC()), true)
		queryAfterTT := TD2UT(Date2JDE(eventTime.Add(time.Hour).UTC()), true)

		exactNext := NextMoonPlanetConjunction(queryAtTT, planet)
		exactClosest := ClosestMoonPlanetConjunction(queryAtTT, planet)
		exactLastAfter := LastMoonPlanetConjunction(queryAfterTT, planet)

		for name, gotUT := range map[string]float64{
			"exactNext":      exactNext,
			"exactClosest":   exactClosest,
			"lastAfterEvent": exactLastAfter,
		} {
			gotTime := JDE2DateByZone(gotUT, time.UTC, false)
			if diff := math.Abs(gotUT - eventUT); diff > 1e-9 {
				t.Fatalf("%s %s mismatch: got %s want %s diff=%v", sample.Planet, name, gotTime.Format(time.RFC3339Nano), eventTime.Format(time.RFC3339Nano), diff*86400)
			}
		}
	}
}

func TestMoonPlanetConjunctionRejectsOppositionBranchJump(t *testing.T) {
	query := time.Date(1900, 11, 10, 12, 0, 0, 0, time.UTC)
	queryTT := TD2UT(Date2JDE(query), true)

	lastUT := LastMoonPlanetConjunction(queryTT, MoonPlanetConjunctionSaturn)
	nextUT := NextMoonPlanetConjunction(queryTT, MoonPlanetConjunctionSaturn)

	if math.Abs(lastUT-Date2JDE(query)) <= 5.0/86400.0 {
		t.Fatalf("last returned query time on branch jump: got %s", JDE2DateByZone(lastUT, time.UTC, false).Format(time.RFC3339Nano))
	}
	if math.Abs(nextUT-Date2JDE(query)) <= 5.0/86400.0 {
		t.Fatalf("next returned query time on branch jump: got %s", JDE2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano))
	}

	for name, gotUT := range map[string]float64{
		"last": lastUT,
		"next": nextUT,
	} {
		delta := math.Abs(moonPlanetConjunctionDeltaAt(TD2UT(gotUT, true), MoonPlanetConjunctionSaturn, -1))
		if delta > moonPlanetConjunctionEventTolerance {
			t.Fatalf("%s returned non-event candidate: delta=%.8f event=%s", name, delta, JDE2DateByZone(gotUT, time.UTC, false).Format(time.RFC3339Nano))
		}
	}
}

func TestMoonPlanetConjunctionDirectionalOrderingOnSampleQueries(t *testing.T) {
	samples := []struct {
		planet MoonPlanetConjunctionPlanet
		query  time.Time
	}{
		{planet: MoonPlanetConjunctionSaturn, query: time.Date(1700, 4, 15, 12, 0, 0, 0, time.UTC)},
		{planet: MoonPlanetConjunctionMercury, query: time.Date(1900, 1, 14, 12, 0, 0, 0, time.UTC)},
		{planet: MoonPlanetConjunctionVenus, query: time.Date(1950, 6, 3, 12, 0, 0, 0, time.UTC)},
		{planet: MoonPlanetConjunctionMars, query: time.Date(2000, 2, 29, 18, 0, 0, 0, time.UTC)},
		{planet: MoonPlanetConjunctionJupiter, query: time.Date(2026, 5, 20, 0, 0, 0, 0, time.UTC)},
		{planet: MoonPlanetConjunctionSaturn, query: time.Date(2100, 8, 17, 6, 0, 0, 0, time.UTC)},
		{planet: MoonPlanetConjunctionUranus, query: time.Date(2200, 11, 2, 9, 0, 0, 0, time.UTC)},
		{planet: MoonPlanetConjunctionNeptune, query: time.Date(2300, 4, 24, 3, 0, 0, 0, time.UTC)},
	}

	for _, sample := range samples {
		queryTT := TD2UT(Date2JDE(sample.query.UTC()), true)
		lastUT := LastMoonPlanetConjunction(queryTT, sample.planet)
		nextUT := NextMoonPlanetConjunction(queryTT, sample.planet)
		closestUT := ClosestMoonPlanetConjunction(queryTT, sample.planet)

		if math.IsNaN(lastUT) || math.IsNaN(nextUT) || math.IsNaN(closestUT) {
			t.Fatalf("planet=%v query=%s returned NaN event(s): last=%v next=%v closest=%v", sample.planet, sample.query.Format(time.RFC3339), lastUT, nextUT, closestUT)
		}
		if !eventUTQueryBeforeOrEqual(lastUT, queryTT) {
			t.Fatalf("planet=%v last after query: last=%s query=%s", sample.planet, JDE2DateByZone(lastUT, time.UTC, false).Format(time.RFC3339Nano), sample.query.Format(time.RFC3339Nano))
		}
		if !eventUTQueryAfterOrEqual(nextUT, queryTT) {
			t.Fatalf("planet=%v next before query: next=%s query=%s", sample.planet, JDE2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano), sample.query.Format(time.RFC3339Nano))
		}
		if closestUT != closestEventUTToQueryTT(queryTT, lastUT, nextUT) {
			t.Fatalf("planet=%v closest mismatch: got=%s want=%s", sample.planet, JDE2DateByZone(closestUT, time.UTC, false).Format(time.RFC3339Nano), JDE2DateByZone(closestEventUTToQueryTT(queryTT, lastUT, nextUT), time.UTC, false).Format(time.RFC3339Nano))
		}
		for name, gotUT := range map[string]float64{
			"last":    lastUT,
			"next":    nextUT,
			"closest": closestUT,
		} {
			delta := math.Abs(moonPlanetConjunctionDeltaAt(TD2UT(gotUT, true), sample.planet, -1))
			if delta > moonPlanetConjunctionEventTolerance {
				t.Fatalf("planet=%v %s returned non-event candidate: delta=%.8f event=%s", sample.planet, name, delta, JDE2DateByZone(gotUT, time.UTC, false).Format(time.RFC3339Nano))
			}
		}
	}
}

func TestMoonPlanetConjunctionKeepsImmediateNeighborEvents(t *testing.T) {
	query := time.Date(1700, 4, 15, 12, 0, 0, 0, time.UTC)
	queryTT := TD2UT(Date2JDE(query.UTC()), true)

	lastUT := LastMoonPlanetConjunction(queryTT, MoonPlanetConjunctionSaturn)
	nextUT := NextMoonPlanetConjunction(queryTT, MoonPlanetConjunctionSaturn)
	closestUT := ClosestMoonPlanetConjunction(queryTT, MoonPlanetConjunctionSaturn)

	wantLast := time.Date(1700, 4, 15, 11, 55, 59, 115569293, time.UTC)
	wantNext := time.Date(1700, 5, 13, 0, 35, 5, 981616675, time.UTC)
	const tolerance = 5.0 / 86400.0

	if diff := math.Abs(lastUT - Date2JDE(wantLast)); diff > tolerance {
		t.Fatalf("last mismatch: got=%s want=%s diff=%.3fs", JDE2DateByZone(lastUT, time.UTC, false).Format(time.RFC3339Nano), wantLast.Format(time.RFC3339Nano), diff*86400)
	}
	if diff := math.Abs(nextUT - Date2JDE(wantNext)); diff > tolerance {
		t.Fatalf("next mismatch: got=%s want=%s diff=%.3fs", JDE2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano), wantNext.Format(time.RFC3339Nano), diff*86400)
	}
	if !sameEventJD(closestUT, lastUT) {
		t.Fatalf("closest should keep immediate previous event: closest=%s last=%s", JDE2DateByZone(closestUT, time.UTC, false).Format(time.RFC3339Nano), JDE2DateByZone(lastUT, time.UTC, false).Format(time.RFC3339Nano))
	}
}

func TestMoonPlanetConjunctionNextAdvancesPastReturnedEvent(t *testing.T) {
	seed := TD2UT(Date2JDE(time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)), true)
	eventUT := NextMoonPlanetConjunction(seed, MoonPlanetConjunctionMercury)
	query := JDE2DateByZone(eventUT, time.UTC, false).Add(time.Second)
	queryTT := TD2UT(Date2JDE(query.UTC()), true)

	nextUT := NextMoonPlanetConjunction(queryTT, MoonPlanetConjunctionMercury)
	if eventUTQueryTTDelta(nextUT, queryTT) <= 0 {
		t.Fatalf("expected next conjunction after query: query=%s next=%s delta=%.6fs",
			query.Format(time.RFC3339Nano),
			JDE2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano),
			eventUTQueryTTDelta(nextUT, queryTT)*86400,
		)
	}
	if sameEventJD(nextUT, eventUT) {
		t.Fatalf("next conjunction should advance to a later event: event=%s next=%s",
			JDE2DateByZone(eventUT, time.UTC, false).Format(time.RFC3339Nano),
			JDE2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano),
		)
	}
}
