import assert from "node:assert/strict";
import { mkdtemp, readFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";

import {
  MAP_REFERENCE_SEARCH_SCHEMA,
  MAP_REFERENCE_SNAPSHOT_SCHEMA,
  TRANSPORT_STATION_PROFILE_ID,
  createReferenceStationSource,
} from "../src/reference-station-source.mjs";

const root = resolve(fileURLToPath(new URL("..", import.meta.url)));
const seedFile = join(root, "reference-sources", "transport-stations", "moscow-v1.json");

test("packaged transport station profile is normalized and provider-neutral", async () => {
  const cacheDir = await mkdtemp(join(tmpdir(), "nodedc-reference-stations-"));
  const source = await createReferenceStationSource({ seedFile, cacheDir, fetchEnabled: false });
  const snapshot = await source.snapshot();
  assert.equal(snapshot.schemaVersion, MAP_REFERENCE_SNAPSHOT_SCHEMA);
  assert.equal(snapshot.profileId, TRANSPORT_STATION_PROFILE_ID);
  assert.equal(snapshot.complete, true);
  assert.equal(snapshot.metadata.factCount, 638);
  assert.deepEqual(snapshot.metadata.categoryCounts, {
    metro: 273,
    railway_station: 357,
    railway_terminal: 8,
  });
  assert.ok(snapshot.facts.every((fact) => !JSON.stringify(fact).match(/https?:|token|credential|endpoint/i)));
  assert.ok(snapshot.facts.every((fact) => ["map.station", "map.terminal"].includes(fact.semanticType)));
});

test("bbox inside the seed spatial-cell coverage never waits for upstream", async () => {
  const cacheDir = await mkdtemp(join(tmpdir(), "nodedc-reference-stations-"));
  let calls = 0;
  const source = await createReferenceStationSource({
    seedFile,
    cacheDir,
    cellDegrees: 0.5,
    fetchImpl: async () => {
      calls += 1;
      throw new Error("seed coverage must not call upstream");
    },
  });
  const snapshot = await source.snapshot({ bbox: [36.9, 55.4, 38.2, 56.1] });
  assert.equal(calls, 0);
  assert.equal(snapshot.complete, true);
  assert.equal(snapshot.metadata.requestedCellCount, 12);
  assert.ok(snapshot.facts.length > 0);
});

test("bbox fetch uses one cached spatial cell and never exposes upstream payload", async () => {
  const cacheDir = await mkdtemp(join(tmpdir(), "nodedc-reference-stations-"));
  let calls = 0;
  const source = await createReferenceStationSource({
    seedFile,
    cacheDir,
    cellDegrees: 1,
    fetchImpl: async () => {
      calls += 1;
      return new Response(JSON.stringify({
        elements: [{
          type: "node",
          id: 987654321,
          lat: 59.93,
          lon: 30.31,
          tags: { railway: "station", station: "subway", name: "Тестовая", website: "https://not-projected.invalid" },
        }],
      }), { status: 200, headers: { "content-type": "application/json" } });
    },
  });
  const first = await source.snapshot({ bbox: [30, 59, 31, 60] });
  const second = await source.snapshot({ bbox: [30, 59, 31, 60] });
  assert.equal(calls, 1);
  assert.ok(first.facts.some((fact) => fact.sourceId === "osm.node.987654321"));
  assert.deepEqual(first.facts, second.facts);
  assert.ok(!JSON.stringify(first).includes("not-projected"));
  const cached = JSON.parse(await readFile(join(cacheDir, "reference-features", TRANSPORT_STATION_PROFILE_ID, "30_59.json"), "utf8"));
  assert.equal(cached.schemaVersion, "nodedc.map-reference-cell/v1");
});

test("viewport cells use bounded concurrency and deduplicate inflight fetches", async () => {
  const cacheDir = await mkdtemp(join(tmpdir(), "nodedc-reference-stations-"));
  let active = 0;
  let maximumActive = 0;
  let calls = 0;
  const starts = [];
  const source = await createReferenceStationSource({
    seedFile,
    cacheDir,
    cellDegrees: 0.5,
    fetchImpl: async () => {
      calls += 1;
      starts.push(Date.now());
      active += 1;
      maximumActive = Math.max(maximumActive, active);
      await new Promise((resolveDelay) => setTimeout(resolveDelay, 15));
      active -= 1;
      return new Response(JSON.stringify({ elements: [] }), {
        status: 200,
        headers: { "content-type": "application/json" },
      });
    },
  });
  const bbox = [29.9, 59.7, 30.9, 60.2];
  await Promise.all([source.snapshot({ bbox }), source.snapshot({ bbox })]);
  assert.equal(calls, 6);
  assert.ok(maximumActive <= 2);
  assert.ok(starts.slice(1).every((startedAt, index) => startedAt - starts[index] >= 650));
  const status = await source.status();
  assert.equal(status.activeFetches, 0);
  assert.equal(status.queuedFetches, 0);
  assert.equal(status.upstreamState, "ready");
});

test("Russian name wins and upstream failures are safe diagnostics", async () => {
  const cacheDir = await mkdtemp(join(tmpdir(), "nodedc-reference-stations-"));
  let fail = false;
  const source = await createReferenceStationSource({
    seedFile,
    cacheDir,
    cellDegrees: 1,
    fetchImpl: async () => {
      if (fail) throw new TypeError("getaddrinfo EAI_AGAIN overpass.internal.example");
      return new Response(JSON.stringify({
        elements: [{
          type: "node",
          id: 987654322,
          lat: 59.93,
          lon: 30.31,
          tags: {
            railway: "station",
            station: "subway",
            name: "Petrogradskaya",
            "name:ru": "Петроградская",
          },
        }],
      }), { status: 200, headers: { "content-type": "application/json" } });
    },
  });
  const snapshot = await source.snapshot({ bbox: [30, 59, 31, 60] });
  assert.equal(snapshot.facts[0].attributes.name, "Петроградская");

  fail = true;
  await source.snapshot({ bbox: [31, 59, 32, 60] });
  const status = await source.status();
  assert.equal(status.upstreamState, "degraded");
  assert.equal(status.lastFailure, "reference_station_upstream_unavailable");
  assert.ok(status.lastFailureAt);
  assert.ok(!JSON.stringify(status).includes("overpass.internal.example"));
});

test("global station search is normalized, deduplicated and persisted", async () => {
  const cacheDir = await mkdtemp(join(tmpdir(), "nodedc-reference-stations-"));
  let calls = 0;
  const source = await createReferenceStationSource({
    seedFile,
    cacheDir,
    fetchImpl: async (_url, options) => {
      calls += 1;
      const body = new URLSearchParams(options.body);
      assert.match(body.get("data"), /Петроградская/u);
      return new Response(JSON.stringify({
        elements: [{
          type: "node",
          id: 987654323,
          lat: 59.96639,
          lon: 30.3113,
          tags: {
            railway: "station",
            station: "subway",
            name: "Petrogradskaya",
            "name:ru": "Петроградская",
            website: "https://not-projected.invalid",
          },
        }],
      }), { status: 200, headers: { "content-type": "application/json" } });
    },
  });
  const first = await source.search({ query: "Петроградская", limit: 8 });
  const second = await source.search({ query: "Петроградская", limit: 8 });
  assert.equal(first.schemaVersion, MAP_REFERENCE_SEARCH_SCHEMA);
  assert.equal(first.complete, true);
  assert.equal(calls, 1);
  assert.deepEqual(first.facts, second.facts);
  assert.equal(first.facts[0].attributes.name, "Петроградская");
  assert.ok(!JSON.stringify(first).includes("not-projected"));
  const persisted = JSON.parse(await readFile(
    join(cacheDir, "reference-features", `${TRANSPORT_STATION_PROFILE_ID}.search.json`),
    "utf8",
  ));
  assert.equal(persisted.schemaVersion, "nodedc.map-reference-search-index/v1");

  const restarted = await createReferenceStationSource({
    seedFile,
    cacheDir,
    fetchEnabled: false,
  });
  const cached = await restarted.search({ query: "Петроградская", limit: 8 });
  assert.equal(cached.complete, true);
  assert.equal(cached.facts[0].sourceId, "osm.node.987654323");
});
