package basic

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

func TestStarOccultationLatitudeEnvelopePrefilter(t *testing.T) {
	start := occultationTimeToTT(time.Date(2026, 8, 1, 0, 0, 0, 0, time.UTC))
	end := start + starOccultationSiderealMonthDays
	polar := StarCoordinate{RA: 0, Dec: 89, Epoch: time.Date(2000, 1, 1, 12, 0, 0, 0, time.UTC), Frame: CoordinateFrameICRS}
	if starOccultationLatitudeEnvelopePass(start, end, polar, nil, 0) {
		t.Fatal("polar star should be rejected by global ecliptic-latitude envelope")
	}
	if !starOccultationLatitudeEnvelopePass(start, end, hr4799OccultationCoordinateForTest(), nil, 0) {
		t.Fatal("HR 4799 should pass the global ecliptic-latitude envelope")
	}
}

func TestStarOccultationScanCandidatesFindsMultipleLocalMinima(t *testing.T) {
	value := func(tt float64) float64 {
		return (tt-1)*(tt-1)*(tt-3)*(tt-3) + 0.001
	}
	got := starOccultationScanCandidates(0, 4, 0.5, value, func(float64) bool { return true })
	if len(got) != 2 {
		t.Fatalf("candidate count = %d, want 2: %v", len(got), got)
	}
	if math.Abs(got[0]-1) > 1e-6 || math.Abs(got[1]-3) > 1e-6 {
		t.Fatalf("candidate minima = %v, want [1 3]", got)
	}
}

func TestStarOccultationScanCandidatesFindsNarrowWindowMidpoint(t *testing.T) {
	const (
		start = 10.0
		end   = 10.1
		want  = (start + end) / 2
	)
	value := func(tt float64) float64 { return (tt - want) * (tt - want) }
	got := starOccultationScanCandidates(start, end, starOccultationDefaultStepDays, value, func(float64) bool { return true })
	if len(got) != 1 {
		t.Fatalf("candidate count = %d, want 1: %v", len(got), got)
	}
	if math.Abs(got[0]-want) > 1e-7 {
		t.Fatalf("candidate midpoint = %.12f, want %.12f", got[0], want)
	}
}

func TestUniqueOccultationCandidateTimesFiltersSingleOutsideCandidate(t *testing.T) {
	if got := uniqueOccultationCandidateTimes([]float64{9.9}, 10, 11); len(got) != 0 {
		t.Fatalf("outside candidate was not filtered: %v", got)
	}
	got := uniqueOccultationCandidateTimes([]float64{10.5}, 10, 11)
	if len(got) != 1 || got[0] != 10.5 {
		t.Fatalf("inside candidate = %v, want [10.5]", got)
	}
}

func TestStarOccultationPropagatesApparentCoordinateProperMotion(t *testing.T) {
	epoch := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
	target := epoch.Add(365*24*time.Hour + 6*time.Hour)
	star := StarCoordinate{
		RA:                             10,
		Dec:                            20,
		Epoch:                          epoch,
		Frame:                          CoordinateFrameApparentOfDate,
		ProperMotionRACosDecMasPerYear: 360000,
		ProperMotionDecMasPerYear:      -720000,
	}
	baseline := star
	baseline.ProperMotionRACosDecMasPerYear = 0
	baseline.ProperMotionDecMasPerYear = 0
	baselineRA, baselineDec := starApparentRaDec(occultationTimeToTT(target), baseline, Observer{})
	ra, dec := starApparentRaDec(occultationTimeToTT(target), star, Observer{})
	raMotion := signedAngleDifference(ra, baselineRA) * math.Cos(baselineDec*math.Pi/180)
	decMotion := dec - baselineDec
	if math.Abs(raMotion-0.1) > 0.001 {
		t.Fatalf("propagated RA*cos(Dec) motion = %.10f deg, want 0.1", raMotion)
	}
	if math.Abs(decMotion-(-0.2)) > 0.001 {
		t.Fatalf("propagated Dec motion = %.10f deg, want -0.2", decMotion)
	}
}

func TestStarOccultationApparentOfDateRoundTripsAtEpoch(t *testing.T) {
	epoch := time.Date(2026, 8, 2, 12, 0, 0, 0, time.UTC)
	star := StarCoordinate{RA: 189.5, Dec: -6.2, Epoch: epoch, Frame: CoordinateFrameApparentOfDate, ParallaxMas: 100}
	ra, dec := starApparentRaDecGeocentric(occultationTimeToTT(epoch), star)
	if math.Abs(signedAngleDifference(ra, star.RA))*3600 > 1e-5 || math.Abs(dec-star.Dec)*3600 > 1e-5 {
		t.Fatalf("apparent coordinate did not round-trip at epoch: got %.12f %.12f", ra, dec)
	}
}

func TestStarOccultationApparentPlaceCorrections(t *testing.T) {
	location := time.FixedZone("CST", 8*3600)
	tt := occultationTimeToTT(time.Date(2025, 6, 5, 20, 2, 7, 700000000, location))
	star := hr4799OccultationCoordinateForTest()

	gotRA, gotDec := starApparentRaDecGeocentric(tt, star)
	if math.Abs(signedAngleDifference(gotRA, 189.527817)) > 0.0002 || math.Abs(gotDec-(-5.973401)) > 0.0002 {
		t.Fatalf("apparent place = %.9f %.9f, want near 189.527817 -5.973401", gotRA, gotDec)
	}

	years := (tt - Date2JDE(star.Epoch.UTC())) / 365.25
	meanRA := star.RA + years*star.ProperMotionRACosDecMasPerYear/(3600000*math.Cos(star.Dec*math.Pi/180))
	meanDec := star.Dec + years*star.ProperMotionDecMasPerYear/3600000
	meanRA, meanDec = Precess(meanRA, meanDec, 2451545, tt)
	correction := angularSeparationDegrees(meanRA, meanDec, gotRA, gotDec) * 3600
	if correction < 5 || correction > 30 {
		t.Fatalf("apparent-place correction = %.6f arcsec, want a plausible annual correction", correction)
	}
}

func TestStarOccultationApparentPlaceAppliesAnnualParallax(t *testing.T) {
	star := hr4799OccultationCoordinateForTest()
	tt := occultationTimeToTT(time.Date(2025, 6, 5, 12, 0, 0, 0, time.UTC))
	withoutRA, withoutDec := starApparentRaDecGeocentric(tt, star)
	star.ParallaxMas = 1000
	withRA, withDec := starApparentRaDecGeocentric(tt, star)
	shift := angularSeparationDegrees(withoutRA, withoutDec, withRA, withDec) * 3600
	if shift < 0.05 || shift > 1.1 {
		t.Fatalf("annual parallax shift = %.6f arcsec, want (0.05, 1.1]", shift)
	}
}

func TestStarOccultationICRSAppliesJ2000FrameBias(t *testing.T) {
	epoch := time.Date(2000, time.January, 1, 12, 0, 0, 0, time.UTC)
	icrs := StarCoordinate{RA: 0, Dec: 0, Epoch: epoch, Frame: CoordinateFrameICRS}
	j2000 := icrs
	j2000.Frame = CoordinateFrameJ2000
	tt := occultationTimeToTT(epoch)
	icrsRA, icrsDec := starApparentRaDecGeocentric(tt, icrs)
	j2000RA, j2000Dec := starApparentRaDecGeocentric(tt, j2000)
	raBiasMas := signedAngleDifference(icrsRA, j2000RA) * 3600000
	decBiasMas := (icrsDec - j2000Dec) * 3600000
	if math.Abs(raBiasMas-14.6) > 0.1 || math.Abs(decBiasMas-(-16.617)) > 0.1 {
		t.Fatalf("ICRS frame bias = %.6f %.6f mas, want about 14.6 -16.617", raBiasMas, decBiasMas)
	}
}

func TestRefinedStarOccultationCenterLineRespectsWidthTolerance(t *testing.T) {
	star := hr4799OccultationCoordinateForTest()
	start := time.Date(2025, time.June, 5, 0, 0, 0, 0, time.UTC)
	paths, err := FindStarOccultationPaths(
		start, start.Add(24*time.Hour), star,
		OccultationPathOptions{Step: 5 * time.Minute, TargetSpacingKM: 50},
	)
	if err != nil || len(paths) != 1 {
		t.Fatalf("FindStarOccultationPaths() paths=%d err=%v, want one", len(paths), err)
	}
	for index, point := range paths[0].CenterLine {
		exact, ok := starOccultationPathCenterPoint(centerTimeTT(point.Time), star, time.UTC)
		if !ok {
			t.Fatalf("exact center point %d is unavailable", index)
		}
		if difference := math.Abs(point.WidthKM - exact.WidthKM); difference > occultationPathWidthToleranceKM {
			t.Fatalf("center point %d width differs from exact value by %.9f km: got %.9f want %.9f",
				index, difference, point.WidthKM, exact.WidthKM)
		}
	}
}

func TestRefineOccultationPathWidthsBoundsSmoothInterpolationError(t *testing.T) {
	start := time.Date(2025, time.January, 1, 0, 0, 0, 0, time.UTC)
	startTT := occultationTimeToTT(start)
	widthAt := func(tt float64) (float64, bool) {
		seconds := (tt - startTT) * 86400
		return 3500 + 0.0002*(seconds-50)*(seconds-50), true
	}
	points := make([]OccultationPathPoint, 101)
	for index := range points {
		points[index].Time = start.Add(time.Duration(index) * time.Second)
	}
	points[0].WidthKM, _ = widthAt(centerTimeTT(points[0].Time))
	points[len(points)-1].WidthKM, _ = widthAt(centerTimeTT(points[len(points)-1].Time))
	refineOccultationPathWidths(points, widthAt)
	for index, point := range points {
		exact, _ := widthAt(centerTimeTT(point.Time))
		if difference := math.Abs(point.WidthKM - exact); difference > occultationPathWidthToleranceKM {
			t.Fatalf("interpolated width %d differs by %.9f km, tolerance %.9f", index, difference, occultationPathWidthToleranceKM)
		}
	}
}

func hr4799OccultationCoordinateForTest() 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,
	}
}
