jest.mock("ol/extent", () => ({
  intersects: (a: number[], b: number[]) =>
    a[0] <= b[2] && a[2] >= b[0] && a[1] <= b[3] && a[3] >= b[1],
}));

jest.mock("ol/proj", () => ({
  toLonLat: (coordinate: number[]) => coordinate,
}));

import {
  TileFeatureIndex,
  buildPipeFeatureFragments,
  clipLineStringPartsToExtent,
  lineStringFromFlatCoordinates,
} from "./tileFeatureIndex";
import { along, bearing, lineString, length } from "@turf/turf";

describe("tileFeatureIndex geometry helpers", () => {
  it("clips a line to the tile core extent without dropping both sides", () => {
    const clipped = clipLineStringPartsToExtent(
      [
        [-10, 5],
        [5, 5],
        [15, 5],
      ],
      [0, 0, 10, 10],
    );

    expect(clipped).toEqual([
      [
        [0, 5],
        [5, 5],
        [10, 5],
      ],
    ]);
  });

  it("preserves all coordinates from a flat line with stride", () => {
    expect(lineStringFromFlatCoordinates([0, 1, 2, 3, 4, 5], 2)).toEqual([
      [0, 1],
      [2, 3],
      [4, 5],
    ]);
  });

  it("keeps disconnected clipped line parts separate", () => {
    expect(
      clipLineStringPartsToExtent(
        [
          [-5, 2],
          [5, 2],
          [15, 2],
          [15, 8],
          [5, 8],
          [-5, 8],
        ],
        [0, 0, 10, 10],
      ),
    ).toEqual([
      [
        [0, 2],
        [5, 2],
        [10, 2],
      ],
      [
        [10, 8],
        [5, 8],
        [0, 8],
      ],
    ]);
  });

  it("keeps multiple tile instances for the same feature id", () => {
    const makeFeature = (flatCoordinates: number[]) => ({
      getProperties: () => ({ id: "P-1", diameter: 100 }),
      getGeometry: () => ({
        getType: () => "LineString",
        getFlatCoordinates: () => flatCoordinates,
        getStride: () => 2,
      }),
    });
    const makeTile = (x: number, flatCoordinates: number[]) => ({
      getTileCoord: () => [14, x, 5],
      getFeatures: () => [makeFeature(flatCoordinates)],
    });
    const source = {
      sourceTiles_: {
        a: makeTile(1, [0, 0, 10, 0]),
        b: makeTile(2, [10, 0, 20, 0]),
      },
      getTileGrid: () => ({
        getTileCoordExtent: ([, x]: [number, number, number]) =>
          x === 1 ? [0, -10, 10, 10] : [10, -10, 20, 10],
      }),
    } as any;

    const index = new TileFeatureIndex("pipes", source);
    const snapshot = index.getSnapshot(undefined, 14);

    expect(snapshot.instances).toHaveLength(2);
    expect(snapshot.instancesById.get("P-1")).toHaveLength(2);
  });

  it("does not reparse an unchanged loaded tile", () => {
    const getFeatures = jest.fn(() => [
      {
        getProperties: () => ({ id: "P-1" }),
        getGeometry: () => ({
          getType: () => "LineString",
          getFlatCoordinates: () => [0, 0, 10, 0],
          getStride: () => 2,
        }),
      },
    ]);
    const tile = {
      getTileCoord: () => [14, 1, 5],
      getRevision: () => 1,
      getFeatures,
    };
    const source = {
      sourceTiles_: { tile },
      getTileGrid: () => ({
        getTileCoordExtent: () => [0, -10, 10, 10],
      }),
    } as any;

    const index = new TileFeatureIndex("pipes", source);
    index.scanLoadedTiles();
    index.scanLoadedTiles();

    expect(getFeatures).toHaveBeenCalledTimes(1);
    expect(index.getSnapshot(undefined, 14).signature).toContain(
      "pipes/14/1/5@1",
    );
  });

  it("builds pipe geometry with the same spherical measurements in one pass", () => {
    const instance = {
      instanceKey: "pipes/14/1/5/0",
      z: 14,
      featureId: "P-1",
      properties: { diameter: 100 },
      geometryType: "LineString",
      tileExtent: [-1, -1, 1, 1] as [number, number, number, number],
      flatCoordinates: [0, 0, 0.01, 0.005, 0.02, 0],
      stride: 2,
    };
    const path = lineString([
      [0, 0],
      [0.01, 0.005],
      [0.02, 0],
    ]);
    const expectedLength = length(path);
    const expectedMidpoint = along(path, expectedLength / 2).geometry
      .coordinates;
    const expectedPrevious = along(path, expectedLength * 0.49).geometry
      .coordinates;
    const expectedNext = along(path, expectedLength * 0.51).geometry
      .coordinates;
    let expectedAngle = -bearing(expectedPrevious, expectedNext) + 90;
    if (expectedAngle < -90 || expectedAngle > 90) expectedAngle += 180;

    const [fragment] = buildPipeFeatureFragments(instance);

    expect(fragment.fragmentLength).toBeCloseTo(expectedLength, 10);
    expect(fragment.position[0]).toBeCloseTo(expectedMidpoint[0], 10);
    expect(fragment.position[1]).toBeCloseTo(expectedMidpoint[1], 10);
    expect(fragment.angle).toBeCloseTo(expectedAngle, 6);
    expect(fragment.timestamps.at(-1)).toBeCloseTo(10, 10);
  });
});
