import { describe, expect, it } from "vitest";
import { parseWktPolygon } from "../tgu-map-2d/geometry/wktParser";
import type { GeoPoint } from "../tgu-map-2d/model/mapTypes";
import {
  COVERAGE_GRID_N,
  COVERAGE_MIN_SAMPLES,
  buildCoverageGrid,
  coverageOf,
  coverageOfMasks,
  rankByMarginalGain,
  sampleSlotMask,
  unionMasks,
} from "./coverage";

/** Прямоугольное кольцо (замкнутое) — основной строительный блок фикстур. */
function rect(minLon: number, minLat: number, maxLon: number, maxLat: number): GeoPoint[] {
  return [
    { lon: minLon, lat: minLat },
    { lon: maxLon, lat: minLat },
    { lon: maxLon, lat: maxLat },
    { lon: minLon, lat: maxLat },
    { lon: minLon, lat: minLat },
  ];
}

describe("buildCoverageGrid", () => {
  it("квадрат-заявка → все центры ячеек внутри (sampleCount = N²)", () => {
    const grid = buildCoverageGrid([rect(0, 0, 10, 10)]);
    expect(grid).not.toBeNull();
    expect(grid!.n).toBe(COVERAGE_GRID_N);
    expect(grid!.sampleCount).toBe(COVERAGE_GRID_N * COVERAGE_GRID_N);
    expect(grid!.requestWeight).toBeGreaterThan(0);
  });

  it("пустой набор колец → null", () => {
    expect(buildCoverageGrid([])).toBeNull();
  });

  it("кольцо <3 точек → null (дегенерат)", () => {
    expect(buildCoverageGrid([[{ lon: 0, lat: 0 }, { lon: 1, lat: 1 }]])).toBeNull();
  });

  it("нулевая площадь (коллинеарные точки) → 0 сэмплов → null", () => {
    expect(
      buildCoverageGrid([[{ lon: 0, lat: 0 }, { lon: 10, lat: 0 }, { lon: 5, lat: 0 }]]),
    ).toBeNull();
  });
});

describe("sampleSlotMask + coverageOf", () => {
  const grid = buildCoverageGrid([rect(0, 0, 10, 10)])!;

  it("слот = заявка → ~100% (центр-сэмплинг не теряет краевые ячейки)", () => {
    const mask = sampleSlotMask(grid, [rect(0, 0, 10, 10)]);
    expect(coverageOf(grid, mask)!.percent).toBeGreaterThanOrEqual(99);
  });

  it("слот = левая половина → ~50%", () => {
    const mask = sampleSlotMask(grid, [rect(0, 0, 5, 10)]);
    const pct = coverageOf(grid, mask)!.percent;
    expect(pct).toBeGreaterThanOrEqual(45);
    expect(pct).toBeLessThanOrEqual(55);
  });

  it("слот вне bbox заявки (bbox-прунинг) → 0%", () => {
    const mask = sampleSlotMask(grid, [rect(100, 100, 110, 110)]);
    expect(coverageOf(grid, mask)!.percent).toBe(0);
    expect(mask.every((v) => v === 0)).toBe(true);
  });

  it("битый контур (кольцо <3 точек) → пустая маска, слот не участвует", () => {
    const mask = sampleSlotMask(grid, [[{ lon: 0, lat: 0 }, { lon: 5, lat: 5 }]]);
    expect(mask.every((v) => v === 0)).toBe(true);
  });
});

describe("coverageOfMasks (booked∪selected)", () => {
  const grid = buildCoverageGrid([rect(0, 0, 10, 10)])!;

  it("пустой набор → 0%", () => {
    expect(coverageOfMasks(grid, [])!.percent).toBe(0);
  });

  it("две одинаковые маски (OR de-dup) → ~100%, не 200%", () => {
    const full = sampleSlotMask(grid, [rect(0, 0, 10, 10)]);
    const res = coverageOfMasks(grid, [full, full])!;
    expect(res.percent).toBeLessThanOrEqual(100);
    expect(res.percent).toBeGreaterThanOrEqual(99);
  });

  it("левая ∪ правая половины → ~100%", () => {
    const left = sampleSlotMask(grid, [rect(0, 0, 5, 10)]);
    const right = sampleSlotMask(grid, [rect(5, 0, 10, 10)]);
    expect(coverageOfMasks(grid, [left, right])!.percent).toBeGreaterThanOrEqual(99);
  });

  it("union частично перекрывающихся слотов не двойного счёта", () => {
    const a = sampleSlotMask(grid, [rect(0, 0, 6, 10)]);
    const b = sampleSlotMask(grid, [rect(4, 0, 10, 10)]);
    const union = unionMasks(grid, [a, b]);
    expect(coverageOf(grid, union)!.percent).toBeGreaterThanOrEqual(99);
  });
});

describe("rankByMarginalGain (weighted)", () => {
  const grid = buildCoverageGrid([rect(0, 0, 10, 10)])!;
  const big = sampleSlotMask(grid, [rect(0, 0, 6, 10)]); // 60%
  const small = sampleSlotMask(grid, [rect(0, 0, 4, 10)]); // 40%

  it("жадный порядок: больший вклад — первым", () => {
    const ranked = rankByMarginalGain(grid, [
      { key: "small", mask: small },
      { key: "big", mask: big },
    ]);
    expect(ranked[0]!.key).toBe("big");
    expect(ranked[0]!.gainPercent).toBeGreaterThan(ranked[1]!.gainPercent);
  });

  it("тай-брейк: равный вклад → исходный порядок", () => {
    const left = sampleSlotMask(grid, [rect(0, 0, 5, 10)]);
    const right = sampleSlotMask(grid, [rect(5, 0, 10, 10)]);
    const ranked = rankByMarginalGain(grid, [
      { key: "left", mask: left },
      { key: "right", mask: right },
    ]);
    expect(ranked.map((r) => r.key)).toEqual(["left", "right"]);
  });

  it("вклад 0: кандидат — подмножество уже покрытого → gainPercent 0 («—»)", () => {
    const base = sampleSlotMask(grid, [rect(0, 0, 10, 10)]); // всё покрыто
    const ranked = rankByMarginalGain(grid, [{ key: "redundant", mask: small }], base);
    expect(ranked[0]!.gainPercent).toBe(0);
  });
});

describe("антимеридиан (собственный unwrap-фрейм)", () => {
  it("заявка и слот через 180° → корректные ~100%", () => {
    // Кольцо пересекает антимеридиан: долготы 170 и -170 (=190 в unwrap-фрейме).
    const ring: GeoPoint[] = [
      { lon: 170, lat: 0 },
      { lon: -170, lat: 0 },
      { lon: -170, lat: 10 },
      { lon: 170, lat: 10 },
      { lon: 170, lat: 0 },
    ];
    const grid = buildCoverageGrid([ring]);
    expect(grid).not.toBeNull();
    expect(grid!.sampleCount).toBe(COVERAGE_GRID_N * COVERAGE_GRID_N);
    const mask = sampleSlotMask(grid!, [ring]);
    expect(coverageOf(grid!, mask)!.percent).toBeGreaterThanOrEqual(99);
  });
});

describe("WKT holes — лимит MVP-парсера зафиксирован", () => {
  it("полигон с дырой парсится только во внешнее кольцо (1 кольцо)", () => {
    const rings = parseWktPolygon(
      "POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (2 2, 4 2, 4 4, 2 4, 2 2))",
    );
    expect(rings).toHaveLength(1);
  });

  it("дыра игнорируется → покрытие считает её закрытой (~100%)", () => {
    const rings = parseWktPolygon(
      "POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (2 2, 4 2, 4 4, 2 4, 2 2))",
    );
    const grid = buildCoverageGrid(rings)!;
    const mask = sampleSlotMask(grid, rings);
    expect(coverageOf(grid, mask)!.percent).toBeGreaterThanOrEqual(99);
  });
});

describe("низкая точность (тонкий полигон)", () => {
  it("крошечные фигуры в большом bbox → мало сэмплов → lowPrecision", () => {
    // Два маленьких треугольника в противоположных углах: bbox 40×40, но суммарная площадь
    // ничтожна → < COVERAGE_MIN_SAMPLES сэмплов внутри (тонкий полигон → абсурдный %).
    const grid = buildCoverageGrid([
      [
        { lon: 0, lat: 0 },
        { lon: 2, lat: 0 },
        { lon: 0, lat: 2 },
        { lon: 0, lat: 0 },
      ],
      [
        { lon: 40, lat: 40 },
        { lon: 40, lat: 38 },
        { lon: 38, lat: 40 },
        { lon: 40, lat: 40 },
      ],
    ]);
    expect(grid).not.toBeNull();
    expect(grid!.sampleCount).toBeGreaterThan(0);
    expect(grid!.sampleCount).toBeLessThan(COVERAGE_MIN_SAMPLES);
    const res = coverageOf(grid!, new Uint8Array(grid!.lons.length));
    expect(res!.lowPrecision).toBe(true);
  });

  it("полный квадрат → точность нормальная", () => {
    const grid = buildCoverageGrid([rect(0, 0, 10, 10)])!;
    expect(coverageOf(grid, new Uint8Array(grid.lons.length))!.lowPrecision).toBe(false);
  });
});
