/**
 * @vitest-environment jsdom
 */

import { describe, it, expect, vi, beforeEach } from "vitest";
import { render } from "@testing-library/react";
import type { IMapMarker } from "../types.js";

// ---------------------------------------------------------------------------
// Leaflet mock
// ---------------------------------------------------------------------------

interface MockMarker {
  icon: unknown;
  tooltipArgs: unknown[] | null;
  addTo: ReturnType<typeof vi.fn>;
  on: ReturnType<typeof vi.fn>;
  bindTooltip: ReturnType<typeof vi.fn>;
  openTooltip: ReturnType<typeof vi.fn>;
  closeTooltip: ReturnType<typeof vi.fn>;
  setIcon: ReturnType<typeof vi.fn>;
  setLatLng: ReturnType<typeof vi.fn>;
  getLatLng: ReturnType<typeof vi.fn>;
  options: { title?: string };
}

interface MockLayerGroup {
  _id: number;
  _markers: MockMarker[];
  addTo: ReturnType<typeof vi.fn>;
  addLayer: ReturnType<typeof vi.fn>;
  removeLayer: ReturnType<typeof vi.fn>;
  clearLayers: ReturnType<typeof vi.fn>;
  hasLayer: ReturnType<typeof vi.fn>;
}

interface MockMap {
  _zoom: number;
  _eventHandlers: Record<string, Array<() => void>>;
  _addedLayers: Set<MockLayerGroup>;
  on: ReturnType<typeof vi.fn>;
  remove: ReturnType<typeof vi.fn>;
  addLayer: ReturnType<typeof vi.fn>;
  removeLayer: ReturnType<typeof vi.fn>;
  hasLayer: ReturnType<typeof vi.fn>;
  getZoom: ReturnType<typeof vi.fn>;
  setZoom: (z: number) => void;
  fireZoomend: () => void;
}

interface MockPolyline {
  latlngs: unknown;
  opts: unknown;
  addTo: ReturnType<typeof vi.fn>;
}

const createdMarkers: MockMarker[] = [];
const createdLayerGroups: MockLayerGroup[] = [];
const createdMaps: MockMap[] = [];
const createdPolylines: MockPolyline[] = [];
let layerGroupCounter = 0;

function resetLeafletMockState() {
  createdMarkers.length = 0;
  createdLayerGroups.length = 0;
  createdMaps.length = 0;
  createdPolylines.length = 0;
  layerGroupCounter = 0;
}

vi.mock("leaflet/dist/leaflet.css", () => ({}));

vi.mock("leaflet", () => {
  function marker(_latlng: unknown, opts: { icon?: unknown; title?: string } = {}) {
    const m: MockMarker = {
      icon: opts.icon,
      tooltipArgs: null,
      addTo: vi.fn((target: { addLayer?: (l: MockMarker) => unknown }) => {
        target.addLayer?.(m);
        return m;
      }),
      on: vi.fn(() => m),
      bindTooltip: vi.fn((...args: unknown[]) => {
        m.tooltipArgs = args;
        return m;
      }),
      openTooltip: vi.fn(() => m),
      closeTooltip: vi.fn(() => m),
      setIcon: vi.fn((ic: unknown) => {
        m.icon = ic;
        return m;
      }),
      setLatLng: vi.fn(() => m),
      getLatLng: vi.fn(() => ({ lat: 0, lng: 0 })),
      options: opts.title !== undefined ? { title: opts.title } : {},
    };
    createdMarkers.push(m);
    return m;
  }

  function layerGroup() {
    const id = layerGroupCounter++;
    const lg: MockLayerGroup = {
      _id: id,
      _markers: [],
      addTo: vi.fn(() => lg),
      addLayer: vi.fn((l: MockMarker) => {
        lg._markers.push(l);
        return lg;
      }),
      removeLayer: vi.fn(() => lg),
      clearLayers: vi.fn(() => {
        lg._markers.length = 0;
        return lg;
      }),
      hasLayer: vi.fn(() => false),
    };
    createdLayerGroups.push(lg);
    return lg;
  }

  function mapFn(_container: unknown, _opts: unknown) {
    const m: MockMap = {
      _zoom: 5,
      _eventHandlers: {},
      _addedLayers: new Set<MockLayerGroup>(),
      on: vi.fn((evt: string, fn: () => void) => {
        m._eventHandlers[evt] ??= [];
        m._eventHandlers[evt]!.push(fn);
        return m;
      }),
      remove: vi.fn(),
      addLayer: vi.fn((l: MockLayerGroup) => {
        m._addedLayers.add(l);
        return m;
      }),
      removeLayer: vi.fn((l: MockLayerGroup) => {
        m._addedLayers.delete(l);
        return m;
      }),
      hasLayer: vi.fn((target: unknown) => {
        if (target && typeof target === "object" && "_markers" in (target as Record<string, unknown>)) {
          return m._addedLayers.has(target as MockLayerGroup);
        }
        const owning = createdLayerGroups.find((lg) =>
          lg._markers.includes(target as MockMarker),
        );
        return owning ? m._addedLayers.has(owning) : false;
      }),
      getZoom: vi.fn(() => m._zoom),
      setZoom: (z: number) => {
        m._zoom = z;
      },
      fireZoomend: () => {
        (m._eventHandlers["zoomend"] ?? []).forEach((h) => h());
      },
    };
    createdMaps.push(m);
    return m;
  }

  function tileLayer() {
    return { addTo: vi.fn(() => ({ addTo: vi.fn() })) };
  }

  function icon(opts: unknown) {
    return { __icon: opts };
  }

  function latLng(lat: number, lng: number) {
    return { lat, lng };
  }

  function polyline(latlngs: unknown, opts: unknown) {
    const p: MockPolyline = {
      latlngs,
      opts,
      addTo: vi.fn((target: { addLayer?: (l: unknown) => unknown }) => {
        target.addLayer?.(p);
        return p;
      }),
    };
    createdPolylines.push(p);
    return p;
  }

  function popup() {
    return {
      setLatLng: vi.fn(function (this: unknown) { return this; }),
      setContent: vi.fn(function (this: unknown) { return this; }),
      addTo: vi.fn(function (this: unknown) { return this; }),
    };
  }

  const L = { marker, layerGroup, map: mapFn, tileLayer, icon, latLng, polyline, popup };
  return { default: L, ...L };
});

// ---------------------------------------------------------------------------
// Imports AFTER the mock
// ---------------------------------------------------------------------------

import { MapCanvas } from "./MapCanvas.js";

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function cat(
  id: string,
  zoomLevel: number,
  countryType: "ru" | "other",
  overrides: Partial<IMapMarker> = {},
): IMapMarker {
  return {
    id,
    lat: 55,
    lng: 37,
    style: "blue-small",
    label: id,
    tooltipPermanent: true,
    zoomLevel,
    countryType,
    ...overrides,
  };
}

function renderCanvas(
  markers: IMapMarker[],
  props: Partial<React.ComponentProps<typeof MapCanvas>> = {},
) {
  return render(
    <MapCanvas
      markers={markers}
      polylines={[]}
      tileUrl="https://tiles.example/{z}/{x}/{y}"
      {...props}
    />,
  );
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe("MapCanvas — legacy (flat) path", () => {
  beforeEach(() => resetLeafletMockState());

  it("renders flat markers without creating zoom-category layers", () => {
    renderCanvas([
      { id: "A", lat: 1, lng: 2, style: "blue-small", label: "A" },
    ]);
    expect(createdMarkers).toHaveLength(1);
    const allMarkers = createdLayerGroups.flatMap((l) => l._markers);
    expect(allMarkers).toContain(createdMarkers[0]);
  });
});

describe("MapCanvas — categorized: visibility predicate", () => {
  beforeEach(() => resetLeafletMockState());

  it("at zoom 3, shows only layers with zoomTier <= 3", () => {
    const markers = [
      cat("m2", 2, "ru"),
      cat("m3", 3, "ru"),
      cat("m5", 5, "ru"),
      cat("m6", 6, "ru"),
    ];
    renderCanvas(markers);

    const map = createdMaps[0]!;
    map.setZoom(3);
    map.fireZoomend();

    const addCalls = map.addLayer.mock.calls.length;
    const removeCalls = map.removeLayer.mock.calls.length;
    expect(addCalls).toBeGreaterThan(0);
    expect(removeCalls).toBeGreaterThan(0);
  });

  it("domestic=true hides countryType=\"other\" layers", () => {
    const markers = [cat("ru2", 2, "ru"), cat("other2", 2, "other")];
    renderCanvas(markers, { domestic: true });

    const map = createdMaps[0]!;
    map.setZoom(6);
    map.fireZoomend();

    const added: MockLayerGroup[] = map.addLayer.mock.calls.map((c) => c[0] as MockLayerGroup);
    const removed: MockLayerGroup[] = map.removeLayer.mock.calls.map((c) => c[0] as MockLayerGroup);

    const otherLayers = createdLayerGroups.filter((l) =>
      l._markers.some((m) => m.options.title === "other2"),
    );
    for (const l of otherLayers) {
      expect(removed).toContain(l);
    }
    const ruLayer = createdLayerGroups.find((l) =>
      l._markers.some((m) => m.options.title === "ru2"),
    );
    expect(ruLayer).toBeDefined();
    expect(added).toContain(ruLayer!);
  });

  it("international=true hides countryType=\"ru\" layers", () => {
    const markers = [cat("ru2", 2, "ru"), cat("other2", 2, "other")];
    renderCanvas(markers, { international: true });

    const map = createdMaps[0]!;
    map.setZoom(6);
    map.fireZoomend();

    const added: MockLayerGroup[] = map.addLayer.mock.calls.map((c) => c[0] as MockLayerGroup);
    const removed: MockLayerGroup[] = map.removeLayer.mock.calls.map((c) => c[0] as MockLayerGroup);

    const ruLayers = createdLayerGroups.filter((l) =>
      l._markers.some((m) => m.options.title === "ru2"),
    );
    for (const l of ruLayers) {
      expect(removed).toContain(l);
    }
    const otherLayer = createdLayerGroups.find((l) =>
      l._markers.some((m) => m.options.title === "other2"),
    );
    expect(added).toContain(otherLayer!);
  });
});

describe("MapCanvas — categorized: highlight layer", () => {
  beforeEach(() => resetLeafletMockState());

  it("highlighted markers go into a distinct always-on layer with orange icon", () => {
    renderCanvas([cat("hi", 6, "ru", { highlighted: true })]);

    const highlightedMarker = createdMarkers[0]!;
    expect(highlightedMarker.icon).toBeDefined();

    const owningLayer = createdLayerGroups.find((l) => l._markers.includes(highlightedMarker));
    expect(owningLayer).toBeDefined();

    const map = createdMaps[0]!;
    expect(map.addLayer.mock.calls.map((c) => c[0]).includes(owningLayer!) || owningLayer!.addTo.mock.calls.length > 0).toBe(true);
  });
});

describe("MapCanvas — tooltip visibility rules", () => {
  beforeEach(() => resetLeafletMockState());

  it("at zoom <= 3, closes non-highlighted tooltips", () => {
    const markers = [cat("a", 2, "ru"), cat("b", 2, "ru", { highlighted: true })];
    renderCanvas(markers);

    const map = createdMaps[0]!;
    map.setZoom(3);
    map.fireZoomend();

    const a = createdMarkers.find((m) => m.options.title === "a")!;
    const b = createdMarkers.find((m) => m.options.title === "b")!;
    expect(a.closeTooltip).toHaveBeenCalled();
    expect(b.closeTooltip).not.toHaveBeenCalled();
  });

  it("with >=2 highlighted markers, closes non-highlighted tooltips at any zoom", () => {
    const markers = [
      cat("a", 2, "ru"),
      cat("b", 2, "ru", { highlighted: true }),
      cat("c", 2, "ru", { highlighted: true }),
    ];
    renderCanvas(markers);

    const map = createdMaps[0]!;
    map.setZoom(5);
    map.fireZoomend();

    const a = createdMarkers.find((m) => m.options.title === "a")!;
    expect(a.closeTooltip).toHaveBeenCalled();
  });

  it("at zoom 4 with 0-1 highlighted, opens all tooltips", () => {
    const markers = [cat("a", 2, "ru"), cat("b", 2, "ru", { highlighted: true })];
    renderCanvas(markers);

    const map = createdMaps[0]!;
    map.setZoom(4);
    map.fireZoomend();

    const a = createdMarkers.find((m) => m.options.title === "a")!;
    const b = createdMarkers.find((m) => m.options.title === "b")!;
    expect(a.openTooltip).toHaveBeenCalled();
    expect(b.openTooltip).toHaveBeenCalled();
  });
});

// ---------------------------------------------------------------------------
// C.3 polyline + intermediate tests
// ---------------------------------------------------------------------------

import type { IMapPolyline } from "../types.js";

function cm(
  id: string,
  countryType: "ru" | "other" = "ru",
  overrides: Partial<IMapMarker> = {},
): IMapMarker {
  return {
    id,
    lat: 55,
    lng: 37,
    style: "blue-small",
    label: id,
    tooltipPermanent: true,
    zoomLevel: 2,
    countryType,
    ...overrides,
  };
}

function pl(id: string, cityIds: string[], style: "direct" | "connecting" = "direct"): IMapPolyline {
  return { id, cityIds, style };
}

describe("MapCanvas — polylines (C.3)", () => {
  beforeEach(() => resetLeafletMockState());

  it("resolves coords from markers and draws a polyline A→B", () => {
    render(
      <MapCanvas
        markers={[cm("A"), cm("B")]}
        polylines={[pl("line", ["A", "B"])]}
        tileUrl="t"
      />,
    );
    const map = createdMaps[0]!;
    map.setZoom(6);
    map.fireZoomend();

    expect(createdPolylines.length).toBeGreaterThanOrEqual(1);
  });

  it("does not draw a polyline when fewer than 2 cities are visible", () => {
    render(
      <MapCanvas
        markers={[cm("A")]}
        polylines={[pl("line", ["A", "B"])]}
        tileUrl="t"
      />,
    );
    const map = createdMaps[0]!;
    map.setZoom(6);
    map.fireZoomend();

    expect(createdPolylines.length).toBe(0);
  });

  it("skips intermediate cities whose marker is not on the map", () => {
    render(
      <MapCanvas
        markers={[cm("A"), cm("B", "other"), cm("C")]}
        polylines={[pl("tri", ["A", "B", "C"])]}
        domestic={true}
        tileUrl="t"
      />,
    );
    const map = createdMaps[0]!;
    // Reset so we count only polylines drawn by the next sync cycle.
    createdPolylines.length = 0;
    map.setZoom(6);
    map.fireZoomend();

    expect(createdPolylines.length).toBe(1);
  });

  it("applies connecting style for non-direct routes", () => {
    render(
      <MapCanvas
        markers={[cm("A"), cm("B")]}
        polylines={[pl("dashed", ["A", "B"], "connecting")]}
        tileUrl="t"
      />,
    );
    const map = createdMaps[0]!;
    map.setZoom(6);
    map.fireZoomend();

    const styled = createdPolylines[0]!;
    const opts = styled.opts as { dashArray?: string } | undefined;
    expect(opts?.dashArray).toBe("4 14");
  });

  it("rebuilds polylines on zoomend", () => {
    render(
      <MapCanvas
        markers={[cm("A"), cm("B")]}
        polylines={[pl("line", ["A", "B"])]}
        tileUrl="t"
      />,
    );
    const map = createdMaps[0]!;
    map.setZoom(6);
    map.fireZoomend();
    const before = createdPolylines.length;

    map.setZoom(4);
    map.fireZoomend();

    expect(createdPolylines.length).toBeGreaterThan(before);
  });

  it("silently skips polylines with unknown city codes", () => {
    render(
      <MapCanvas
        markers={[cm("A")]}
        polylines={[pl("line", ["A", "NOPE"])]}
        tileUrl="t"
      />,
    );
    const map = createdMaps[0]!;
    map.setZoom(6);
    map.fireZoomend();

    expect(createdPolylines.length).toBe(0);
  });
});

describe("MapCanvas — intermediateIds (C.3)", () => {
  beforeEach(() => resetLeafletMockState());

  it("force-opens intermediate marker tooltips even at zoom <= 3", () => {
    render(
      <MapCanvas
        markers={[
          cm("A", "ru", { highlighted: true }),
          cm("B", "ru", { highlighted: true }),
          cm("X", "ru"),
        ]}
        polylines={[]}
        intermediateIds={["X"]}
        tileUrl="t"
      />,
    );
    const map = createdMaps[0]!;
    map.setZoom(3);
    map.fireZoomend();

    const X = createdMarkers.find((m) => m.options.title === "X")!;
    expect(X.openTooltip).toHaveBeenCalled();
  });

  it("force-opens intermediate tooltips with >=2 highlighted markers", () => {
    render(
      <MapCanvas
        markers={[
          cm("A", "ru", { highlighted: true }),
          cm("B", "ru", { highlighted: true }),
          cm("X", "ru"),
        ]}
        polylines={[]}
        intermediateIds={["X"]}
        tileUrl="t"
      />,
    );
    const map = createdMaps[0]!;
    map.setZoom(5);
    map.fireZoomend();

    const X = createdMarkers.find((m) => m.options.title === "X")!;
    expect(X.openTooltip).toHaveBeenCalled();
  });
});
