/**
 * Unit tests for the hand-rolled WKT → GeoJSON parser (#999 risk-zone parser).
 *
 * Highest-risk untested code: this parser drives the risk-zone polygons drawn on
 * the Site Finder map. The bug class it must guard against is coordinate-order
 * inversion (WKT `lon lat` → GeoJSON `[lon, lat]`) — getting that wrong puts
 * zones/markers in the ocean.
 */
import type {
  LineString,
  MultiLineString,
  MultiPolygon,
  Point,
  Polygon,
  Position,
} from "geojson";
import { describe, expect, it } from "vitest";

import { parseRing, splitTopLevel, wktToGeometry } from "../wkt";

// A simple square ring (closed) in `lon lat` order, EKB-ish coordinates.
const SQUARE =
  "POLYGON((60.5 56.8, 60.6 56.8, 60.6 56.9, 60.5 56.9, 60.5 56.8))";

describe("wktToGeometry — POLYGON", () => {
  it("parses a valid single-ring POLYGON", () => {
    const geom = wktToGeometry(SQUARE) as Polygon | null;
    expect(geom).not.toBeNull();
    expect(geom?.type).toBe("Polygon");
    expect(geom?.coordinates).toHaveLength(1); // one outer ring
    expect(geom?.coordinates[0]).toHaveLength(5); // 5 closed points
  });

  it("preserves WKT lon/lat order as GeoJSON [lon, lat]", () => {
    // The bug class: this must NOT swap to [lat, lon].
    const geom = wktToGeometry(SQUARE) as Polygon;
    const first = geom.coordinates[0][0];
    expect(first).toEqual([60.5, 56.8]);
    // lon (60) is the X / first element; lat (56) is the Y / second.
    expect(first[0]).toBe(60.5);
    expect(first[1]).toBe(56.8);
  });

  it("parses a POLYGON with a hole (two rings)", () => {
    const wkt =
      "POLYGON((0 0, 10 0, 10 10, 0 10, 0 0)," + "(2 2, 4 2, 4 4, 2 4, 2 2))";
    const geom = wktToGeometry(wkt) as Polygon | null;
    expect(geom?.type).toBe("Polygon");
    expect(geom?.coordinates).toHaveLength(2); // outer + hole
    expect(geom?.coordinates[1][0]).toEqual([2, 2]);
  });

  it("skips a ring with fewer than 3 points → null when none survive", () => {
    // Two points cannot form a ring; the single ring is filtered out → null.
    const geom = wktToGeometry("POLYGON((0 0, 1 1))");
    expect(geom).toBeNull();
  });
});

describe("wktToGeometry — MULTIPOLYGON", () => {
  it("parses a MULTIPOLYGON with two polygons", () => {
    const wkt =
      "MULTIPOLYGON(" +
      "((0 0, 1 0, 1 1, 0 1, 0 0))," +
      "((5 5, 6 5, 6 6, 5 6, 5 5))" +
      ")";
    const geom = wktToGeometry(wkt) as MultiPolygon | null;
    expect(geom?.type).toBe("MultiPolygon");
    expect(geom?.coordinates).toHaveLength(2); // two polygons
    expect(geom?.coordinates[0][0]).toHaveLength(5);
    expect(geom?.coordinates[1][0][0]).toEqual([5, 5]);
  });

  it("does not let intra-coordinate commas split polygons", () => {
    // The top-level splitter must respect bracket depth — 8 coords, 1 polygon.
    const wkt = "MULTIPOLYGON(((0 0, 1 0, 1 1, 0 1, 0 0)))";
    const geom = wktToGeometry(wkt) as MultiPolygon;
    expect(geom.coordinates).toHaveLength(1);
    expect(geom.coordinates[0]).toHaveLength(1);
  });
});

describe("wktToGeometry — POINT", () => {
  it("parses a POINT preserving lon/lat order", () => {
    const geom = wktToGeometry("POINT(60.6 56.8)") as Point | null;
    expect(geom?.type).toBe("Point");
    expect(geom?.coordinates).toEqual([60.6, 56.8]);
  });

  it("returns null for a POINT with a single coordinate", () => {
    expect(wktToGeometry("POINT(60.6)")).toBeNull();
  });
});

describe("wktToGeometry — EWKT tolerance", () => {
  it("strips a leading SRID=4326; prefix and parses the same geometry", () => {
    const plain = wktToGeometry(SQUARE) as Polygon;
    const ewkt = wktToGeometry("SRID=4326;" + SQUARE) as Polygon | null;
    expect(ewkt?.type).toBe("Polygon");
    expect(ewkt?.coordinates).toEqual(plain.coordinates);
  });

  it("strips the SRID prefix case-insensitively", () => {
    const geom = wktToGeometry("srid=3857;POINT(1 2)") as Point | null;
    expect(geom?.type).toBe("Point");
    expect(geom?.coordinates).toEqual([1, 2]);
  });
});

describe("wktToGeometry — invalid / unsupported input", () => {
  it("returns null for a garbage string", () => {
    expect(wktToGeometry("not wkt at all")).toBeNull();
  });

  it("returns null for an empty string", () => {
    expect(wktToGeometry("")).toBeNull();
  });

  it("returns null for an unsupported geometry type (GEOMETRYCOLLECTION)", () => {
    expect(
      wktToGeometry("GEOMETRYCOLLECTION(POINT(0 0),LINESTRING(0 0,1 1))"),
    ).toBeNull();
  });
});

describe("wktToGeometry — LINESTRING (red lines §13)", () => {
  it("parses a LINESTRING preserving lon/lat order", () => {
    const geom = wktToGeometry(
      "LINESTRING(60.5 56.8, 60.6 56.9, 60.7 56.8)",
    ) as LineString | null;
    expect(geom?.type).toBe("LineString");
    expect(geom?.coordinates).toHaveLength(3);
    // The bug class: WKT lon/lat must stay [lon, lat], not swap to [lat, lon].
    expect(geom?.coordinates[0]).toEqual([60.5, 56.8]);
  });

  it("returns null for a LINESTRING with a single point", () => {
    expect(wktToGeometry("LINESTRING(60.6 56.8)")).toBeNull();
  });
});

describe("wktToGeometry — MULTILINESTRING (red lines §13)", () => {
  it("parses a MULTILINESTRING with two lines", () => {
    const geom = wktToGeometry(
      "MULTILINESTRING((0 0, 1 1, 2 2),(5 5, 6 6))",
    ) as MultiLineString | null;
    expect(geom?.type).toBe("MultiLineString");
    expect(geom?.coordinates).toHaveLength(2);
    expect(geom?.coordinates[0]).toHaveLength(3);
    expect(geom?.coordinates[1][0]).toEqual([5, 5]);
  });

  it("does not let intra-coordinate commas split lines", () => {
    const geom = wktToGeometry(
      "MULTILINESTRING((0 0, 1 1, 2 2))",
    ) as MultiLineString;
    expect(geom.coordinates).toHaveLength(1);
    expect(geom.coordinates[0]).toHaveLength(3);
  });
});

describe("parseRing", () => {
  it("parses a coordinate list into [lon, lat] positions", () => {
    const ring: Position[] = parseRing("60.5 56.8, 60.6 56.9");
    expect(ring).toEqual([
      [60.5, 56.8],
      [60.6, 56.9],
    ]);
  });

  it("drops pairs that are not finite numbers", () => {
    const ring = parseRing("60.5 56.8, foo bar, 1 2");
    expect(ring).toEqual([
      [60.5, 56.8],
      [1, 2],
    ]);
  });
});

describe("splitTopLevel", () => {
  it("splits only on top-level commas, respecting bracket depth", () => {
    const parts = splitTopLevel("(0 0, 1 1),(2 2, 3 3)");
    expect(parts).toHaveLength(2);
    expect(parts[0]).toBe("(0 0, 1 1)");
    expect(parts[1].trim()).toBe("(2 2, 3 3)");
  });
});
