/* Rauchtest mit echtem Zugangstoken gegen die laufende HA-Testinstanz.
   Ergänzt scripts/smoke.ts um alles, was ohne Anmeldung nicht prüfbar ist:
   echte Lesevorgänge, ein echter Dienstaufruf und ein vollständiger Umlauf
   durch die Warteschlange.

   Zugangsdaten kommen aus companion-app/.env.local (schreibt
   testumgebung/aufsetzen.sh). Ohne sie bricht der Test mit einem Hinweis ab,
   ohne zu scheitern - damit er in einer Umgebung ohne Testinstanz nicht
   fälschlich rot wird.

   Aufruf:  npm run smoke:auth  */

import { readFileSync } from "node:fs"
import { fileURLToPath } from "node:url"
import { dirname, resolve } from "node:path"

import { DIENST_DOMAIN, ENTITAETEN } from "../src/api/types.ts"
import { HassRest } from "../src/api/rest.ts"
import { Warteschlange } from "../src/api/warteschlange.ts"
import { ablageSetzen, websocketUrl, type Ablage } from "../src/api/umgebung.ts"

/* ------------------------------------------------------ Zugangsdaten */

const hier = dirname(fileURLToPath(import.meta.url))
const umgebungsdatei = resolve(hier, "../.env.local")

function umgebungLesen(): Record<string, string> {
  try {
    const inhalt = readFileSync(umgebungsdatei, "utf8")
    const werte: Record<string, string> = {}
    for (const zeile of inhalt.split("\n")) {
      const treffer = /^([A-Z_]+)=(.*)$/.exec(zeile.trim())
      if (treffer?.[1] && treffer[2] !== undefined) werte[treffer[1]] = treffer[2]
    }
    return werte
  } catch {
    return {}
  }
}

const umgebung = umgebungLesen()
const BASIS = umgebung["VITE_TEST_URL"] ?? "http://localhost:18123"
const TOKEN = umgebung["VITE_TEST_TOKEN"] ?? process.env["VITE_TEST_TOKEN"] ?? ""

if (!TOKEN) {
  console.log("übersprungen: kein Token in companion-app/.env.local")
  console.log("               testumgebung/aufsetzen.sh ausführen, dann erneut versuchen.")
  process.exit(0)
}

/* ------------------------------------------------------------ Rahmen */

let fehler = 0
function pruefe(bezeichnung: string, bedingung: boolean, zusatz = ""): void {
  console.log(`${bedingung ? "PASS" : "FAIL"}  ${bezeichnung}${zusatz ? `  (${zusatz})` : ""}`)
  if (!bedingung) fehler += 1
}

/** Ablage im Arbeitsspeicher — Node hat kein localStorage. */
class SpeicherAblage implements Ablage {
  #werte = new Map<string, string>()
  async lesen(schluessel: string) {
    return this.#werte.get(schluessel) ?? null
  }
  async schreiben(schluessel: string, wert: string) {
    this.#werte.set(schluessel, wert)
  }
  async loeschen(schluessel: string) {
    this.#werte.delete(schluessel)
  }
}
ablageSetzen(new SpeicherAblage())

const zugang = { basisUrl: BASIS, token: TOKEN }
const rest = new HassRest(zugang)

/* ------------------------------------------------------------- Lesen */

try {
  const antwort = await rest.verbindungPruefen()
  pruefe("verbindungPruefen mit Token", typeof antwort.message === "string", antwort.message)
} catch (e) {
  pruefe("verbindungPruefen mit Token", false, e instanceof Error ? e.message : String(e))
}

try {
  const profil = await rest.datenLesen<Record<string, unknown>>(ENTITAETEN.profil)
  pruefe(
    "Profil liefert die erwarteten Abschnitte",
    Boolean(profil["fahrzeug"] && profil["einstellungen"] && profil["reifen"]),
    Object.keys(profil).join(", "),
  )
} catch (e) {
  pruefe("Profil lesen", false, e instanceof Error ? e.message : String(e))
}

try {
  const status = await rest.datenLesen<Record<string, unknown>>(ENTITAETEN.fahrzeugstatus)
  /* Die Feldnamen sind der Grund für diesen Test: ein früherer Entwurf der
     Typen hatte tank_prozent/sicher_abgestellt/sicherheit angenommen, das
     Backend schreibt aber tankprozent/gesichert/sicherheitscheck. */
  pruefe(
    "Fahrzeugstatus benutzt die erwarteten Feldnamen",
    "tankprozent" in status && "gesichert" in status && "sicherheitscheck" in status,
    Object.keys(status).slice(0, 6).join(", "),
  )
  /* Wie viele Punkte es sind, hängt an der Sensor-Zuordnung der Instanz: vier
     Türen, vier Fenster, Heckklappe, Motorhaube — unbelegte Rollen liefern gar
     keinen Eintrag. Eine feste Zahl zu erwarten hieße, die Zuordnung der
     Testinstanz zu prüfen statt das Backend; genau daran scheiterte diese
     Prüfung seit dem Entfernen der Türschloss-Erkennung (16 → 10), ohne dass
     irgendetwas kaputt gewesen wäre. Geprüft wird deshalb die vereinbarte
     Form: label ist Text, ok ist true/false/null (null = Sensor unbekannt). */
  const punkte = (status["sicherheitscheck"] ?? []) as { label?: unknown; ok?: unknown }[]
  pruefe(
    "Sicherheitscheck liefert Punkte in der vereinbarten Form",
    Array.isArray(punkte) &&
      punkte.every((p) => typeof p.label === "string" && (p.ok === null || typeof p.ok === "boolean")),
    `${punkte.length} Punkte`,
  )
} catch (e) {
  pruefe("Fahrzeugstatus lesen", false, e instanceof Error ? e.message : String(e))
}

for (const [name, id] of [
  ["Fahrten", ENTITAETEN.fahrten],
  ["Tankvorgänge", ENTITAETEN.tankvorgaenge],
] as const) {
  try {
    const liste = await rest.datenLesen<unknown[]>(id)
    pruefe(`${name} sind eine Liste`, Array.isArray(liste), `${liste.length} Einträge`)
  } catch (e) {
    pruefe(`${name} lesen`, false, e instanceof Error ? e.message : String(e))
  }
}

/* -------------------------------------------------------- Dienstaufruf */

try {
  await rest.dienstAufrufen(DIENST_DOMAIN, "jetzt_aktualisieren")
  pruefe("Dienstaufruf audi_dashboard.jetzt_aktualisieren", true)
} catch (e) {
  pruefe("Dienstaufruf audi_dashboard.jetzt_aktualisieren", false, e instanceof Error ? e.message : String(e))
}

/* ------------------------------------------------------ Warteschlange */

try {
  const warteschlange = new Warteschlange(rest)
  await warteschlange.einreihen(DIENST_DOMAIN, "jetzt_aktualisieren", {}, "Rauchtest")

  /* einreihen() stößt die Abarbeitung selbst an, ohne sie abzuwarten - ein
     sofortiger eigener abarbeiten()-Aufruf liefe deshalb ins Leere (der Lauf
     ist schon aktiv). Also warten, bis die Schlange tatsächlich leer ist. */
  const frist = Date.now() + 10_000
  while (warteschlange.anzahl > 0 && Date.now() < frist) {
    await new Promise((weiter) => setTimeout(weiter, 100))
  }
  pruefe(
    "Warteschlange arbeitet einen Auftrag vollständig ab",
    warteschlange.anzahl === 0,
    `${warteschlange.anzahl} verblieben`,
  )
} catch (e) {
  pruefe("Warteschlangen-Umlauf", false, e instanceof Error ? e.message : String(e))
}

/* ----------------------------------------- WebSocket mit Anmeldung */

try {
  const ergebnis = await new Promise<string>((aufloesen, ablehnen) => {
    const ws = new WebSocket(websocketUrl(BASIS))
    const frist = setTimeout(() => {
      ablehnen(new Error("Zeitüberschreitung"))
      ws.close()
    }, 10_000)
    ws.onmessage = (nachricht) => {
      const daten = JSON.parse(String(nachricht.data)) as { type: string }
      if (daten.type === "auth_required") {
        ws.send(JSON.stringify({ type: "auth", access_token: TOKEN }))
      } else if (daten.type === "auth_ok" || daten.type === "auth_invalid") {
        clearTimeout(frist)
        aufloesen(daten.type)
        ws.close()
      }
    }
    ws.onerror = () => {
      clearTimeout(frist)
      ablehnen(new Error("WebSocket-Fehler"))
    }
  })
  pruefe("WebSocket meldet sich mit dem Token an", ergebnis === "auth_ok", ergebnis)
} catch (e) {
  pruefe("WebSocket-Anmeldung", false, e instanceof Error ? e.message : String(e))
}

console.log(fehler === 0 ? "\nAlle Prüfungen bestanden." : `\n${fehler} Prüfung(en) fehlgeschlagen.`)
process.exitCode = fehler === 0 ? 0 : 1
