// Prueft astro.js gegen Referenzwerte des U.S. Naval Observatory.
//
// Der Sinn: unsere Rechnung ist eine gekuerzte Fassung des NOAA-Algorithmus.
// Sie gegen eine ausfuehrliche Fassung desselben Algorithmus zu pruefen zeigt
// nur, dass wir zweimal dasselbe gerechnet haben — ein gemeinsamer Denkfehler
// faellt so nie auf. Das USNO rechnet mit JPL-Ephemeriden und ist damit eine
// echte dritte Instanz.
//
// Das Fixture liegt im Repo, der Test geht nicht ins Netz. Neu erzeugen mit
//   node scripts/fetch-usno-reference.mjs <Jahr>

// Muss vor dem ersten Date-Objekt stehen: getSunTimes rechnet auf dem lokalen
// Kalendertag, der Test muss also in der Zeitzone der Station laufen.
process.env.TZ = 'Europe/Berlin'

import { test } from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'

import { getSunTimes, getMoonPhase } from './astro.js'

const ref = JSON.parse(
  readFileSync(new URL('./__fixtures__/usno-reference.json', import.meta.url))
)

const { lat, lon } = ref.station

const MIN = 60000
const ZYKLUS_MS = 29.530588853 * 86400000   // synodischer Monat

// Das USNO rundet auf ganze Minuten. Der wahre Wert liegt also irgendwo in
// [HH:MM-30s, HH:MM+30s] — diese halbe Minute geht in jeden Vergleich ein und
// ist kein Fehler unserer Rechnung.
const RUNDUNG_S = 30

// Toleranzen. Sie sind bewusst knapp ueber den gemessenen Maxima gewaehlt: der
// Test soll anschlagen, wenn jemand die Genauigkeit verschlechtert, nicht erst
// wenn die Anzeige sichtbar falsch wird.
// Gemessen fuer 2026: Sonne max. 45 s, Mond max. 28 min.
const TOLERANZ_SONNE_S = 60
const TOLERANZ_MOND_MIN = 45

const abweichungS = (a, b) => Math.abs(a.getTime() - b.getTime()) / 1000

// "HH:MM" am UTC-Tag `datum` als absoluter Zeitpunkt
function utcZeitpunkt(datum, hhmm) {
  const [j, m, t] = datum.split('-').map(Number)
  const [h, min] = hhmm.split(':').map(Number)
  return new Date(Date.UTC(j, m - 1, t, h, min))
}

test('Zeitzone der Testumgebung ist Europe/Berlin', () => {
  assert.equal(
    new Date('2026-07-01T12:00:00Z').getHours(), 14,
    'TZ greift nicht — der Test wuerde sonst den falschen Kalendertag rechnen'
  )
})

test(`Sonnenauf- und -untergang gegen USNO, ${ref.jahr}`, () => {
  const ausreisser = []
  let maxAuf = 0, maxUnter = 0

  for (const [datum, [auf, unter]] of Object.entries(ref.sonne)) {
    assert.ok(auf && unter, `USNO liefert fuer ${datum} keine Zeiten`)

    // Lokaler Kalendertag — genau so ruft App.jsx die Funktion auf. Auf dieser
    // geografischen Breite fallen Auf- und Untergang eines lokalen Tages immer
    // auf denselben UTC-Tag, der Vergleich ist also direkt moeglich.
    const [j, m, t] = datum.split('-').map(Number)
    const { sunrise, sunset } = getSunTimes(new Date(j, m - 1, t, 12), lat, lon)

    const dAuf   = Math.max(0, abweichungS(sunrise, utcZeitpunkt(datum, auf))   - RUNDUNG_S)
    const dUnter = Math.max(0, abweichungS(sunset,  utcZeitpunkt(datum, unter)) - RUNDUNG_S)

    maxAuf   = Math.max(maxAuf, dAuf)
    maxUnter = Math.max(maxUnter, dUnter)

    if (dAuf > TOLERANZ_SONNE_S || dUnter > TOLERANZ_SONNE_S) {
      ausreisser.push(`${datum}: Aufgang ${dAuf.toFixed(0)}s, Untergang ${dUnter.toFixed(0)}s`)
    }
  }

  console.log(
    `    ${Object.keys(ref.sonne).length} Tage - max. Abweichung: `
    + `Aufgang ${maxAuf.toFixed(0)} s, Untergang ${maxUnter.toFixed(0)} s`
  )
  assert.deepEqual(ausreisser, [], `ueber ${TOLERANZ_SONNE_S} s Abweichung`)
})

test('Sonnenzeiten sind nicht heimlich hoehenkorrigiert', () => {
  // Die frueher eingebaute Korrektur -2.076*sqrt(547)/60 hat die Zeiten um
  // 5-6 min verschoben. Ein einzelner Tag reicht, um sie wiederzuerkennen.
  const datum = '2026-06-21'
  const [auf] = ref.sonne[datum]
  const { sunrise } = getSunTimes(new Date(2026, 5, 21, 12), lat, lon)
  assert.ok(
    abweichungS(sunrise, utcZeitpunkt(datum, auf)) < 3 * 60,
    'Aufgang weicht um Minuten ab — steht die Hoehenkorrektur wieder drin?'
  )
})

test('Zeitumstellung verschiebt den Kalendertag nicht', () => {
  // 00:30 Sommerzeit ist 22:30 UTC des Vortags. Wer auf dem UTC-Tag rechnet,
  // zeigt nachts kurz die Zeiten von gestern.
  for (const datum of ['2026-03-29', '2026-10-25']) {
    const [j, m, t] = datum.split('-').map(Number)
    const mittags = getSunTimes(new Date(j, m - 1, t, 12), lat, lon)
    const nachts  = getSunTimes(new Date(j, m - 1, t, 0, 30), lat, lon)
    assert.equal(
      nachts.sunrise.getTime(), mittags.sunrise.getTime(),
      `${datum}: 00:30 und 12:00 liefern verschiedene Tage`
    )
  }
})

// Sucht den Zeitpunkt, an dem unsere Rechnung die Zielphase erreicht, per
// Bisektion um den USNO-Zeitpunkt herum. Die Differenz wird zyklisch auf
// [-0.5, 0.5) gebracht, damit sie auch ueber den Neumond hinweg stetig ist.
function unserPhasenzeitpunkt(ziel, umHerum) {
  const diff = zeit => {
    const p = getMoonPhase(new Date(zeit)).phase
    return ((((p - ziel + 0.5) % 1) + 1) % 1) - 0.5
  }

  let lo = umHerum.getTime() - 2 * 86400000
  let hi = umHerum.getTime() + 2 * 86400000
  if (diff(lo) > 0 || diff(hi) < 0) return null   // kein Nulldurchgang im Fenster

  for (let i = 0; i < 60; i++) {
    const mid = (lo + hi) / 2
    if (diff(mid) < 0) lo = mid; else hi = mid
  }
  return new Date((lo + hi) / 2)
}

const ZIELPHASE = {
  'New Moon': 0,
  'First Quarter': 0.25,
  'Full Moon': 0.5,
  'Last Quarter': 0.75,
}

test(`Mondphasen gegen USNO, ${ref.jahr}`, () => {
  const ausreisser = []
  let max = 0

  for (const { phase, utc } of ref.mondphasen) {
    const ziel = ZIELPHASE[phase]
    assert.ok(ziel !== undefined, `unbekannte Phase "${phase}"`)

    const usno = new Date(utc)
    const unser = unserPhasenzeitpunkt(ziel, usno)
    assert.ok(unser, `${utc}: kein ${phase} im Suchfenster gefunden`)

    const d = Math.abs(unser - usno) / MIN
    max = Math.max(max, d)
    if (d > TOLERANZ_MOND_MIN) ausreisser.push(`${utc} ${phase}: ${d.toFixed(0)} min`)
  }

  console.log(
    `    ${ref.mondphasen.length} Phasen - max. Abweichung: ${max.toFixed(0)} min `
    + `(${((max * MIN) / ZYKLUS_MS * 100).toFixed(2)} % eines Zyklus)`
  )
  assert.deepEqual(ausreisser, [], `ueber ${TOLERANZ_MOND_MIN} min Abweichung`)
})

test('beleuchteter Anteil passt zur Phase', () => {
  for (const { phase, utc } of ref.mondphasen) {
    const { fraction } = getMoonPhase(new Date(utc))
    const erwartet = { 'New Moon': 0, 'Full Moon': 1 }[phase] ?? 0.5
    assert.ok(
      Math.abs(fraction - erwartet) < 0.02,
      `${utc} ${phase}: fraction ${fraction.toFixed(3)}, erwartet ~${erwartet}`
    )
  }
})
