import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { test } from "node:test";
import { JSDOM } from "jsdom";

const webDir = new URL("../../TrajectoryPlanningVisualization/Web/", import.meta.url);
const readWebFile = (name) => readFileSync(new URL(name, webDir), "utf8");

function makeStaticSnapshot({ empty = false } = {}) {
  return {
    sessionNameChinese: "合成轨迹规划观察",
    worldBounds: { xMin: 0, xMax: 12, yMin: 0, yMax: 8 },
    occupancyGrid: null,
    staticPolylines: empty
      ? []
      : [
          {
            id: "global",
            legendChinese: "全局参考路径",
            kind: "global",
            lineStyle: 0,
            points: [
              { x: 0, y: 1 },
              { x: 3, y: 1 },
              { x: 6, y: 2 },
              { x: 8, y: 4 },
              { x: 11, y: 6 }
            ]
          },
          {
            id: "local-g2",
            legendChinese: "完整 Local G2 路径",
            kind: "local-g2",
            lineStyle: 0,
            points: [
              { x: 0.2, y: 1 },
              { x: 3.2, y: 1.1 },
              { x: 6.1, y: 2.1 },
              { x: 8.1, y: 4.1 },
              { x: 10.9, y: 5.9 }
            ]
          }
        ],
    staticMarkers: empty
      ? []
      : [
          { id: "gear-1", kind: "gear-switch", labelChinese: "换向 1", position: { x: 6, y: 2 } },
          { id: "gear-1-end", kind: "gear-switch-end", labelChinese: "换向点 1 / s_end", position: { x: 6, y: 2 } },
          { id: "final-goal", kind: "final-goal", labelChinese: "终点 / s_end", position: { x: 11, y: 6 } }
        ],
    directionSegments: empty
      ? []
      : [
          {
            segmentIndex: 0,
            direction: "forward",
            startsAtGearSwitch: false,
            endsAtGearSwitch: true,
            points: [
              { x: 0, y: 1 },
              { x: 3, y: 1 },
              { x: 6, y: 2 }
            ]
          },
          {
            segmentIndex: 1,
            direction: "reverse",
            startsAtGearSwitch: true,
            endsAtGearSwitch: false,
            points: [
              { x: 6, y: 2 },
              { x: 8, y: 4 },
              { x: 11, y: 6 }
            ]
          }
        ],
    configurationGroups: []
  };
}

function chart(id, title, xAxis, yAxis, points, annotations = []) {
  return {
    id,
    chineseTitle: title,
    xAxisLabel: xAxis,
    yAxisLabel: yAxis,
    noteChinese: "",
    series: [
      {
        id: id + "-current",
        legend: "当前轨迹",
        lineStyle: 0,
        points: points.map(([x, y]) => ({ x, y }))
      }
    ],
    annotations
  };
}

function makeDynamicSnapshot({ empty = false } = {}) {
  if (empty) {
    return {
      sequence: 1,
      observedAtUtc: "2026-08-06T00:00:00Z",
      sessionStateChinese: "运行中",
      activeSegmentIndex: 0,
      activeDirection: "forward",
      vehiclePose: null,
      dynamicPolylines: [],
      dynamicMarkers: [],
      charts: [],
      statusValues: [],
      cycleSummary: {
        cycleVersion: 1,
        occurredAtUtc: "2026-08-06T00:00:00Z",
        status: "成功",
        published: true,
        planningElapsedMilliseconds: 4.2,
        segmentIndex: 0,
        direction: "forward",
        longitudinalMode: "full",
        terminalType: "goal",
        terminalVelocity: 0,
        terminalAcceleration: 0,
        failureReason: ""
      }
    };
  }

  return {
    sequence: 1,
    observedAtUtc: "2026-08-06T00:00:00Z",
    sessionStateChinese: "合成数据运行中",
    activeSegmentIndex: 0,
    activeDirection: "forward",
    vehiclePose: { x: 2, y: 1, headingRadians: 0 },
    dynamicPolylines: [
      {
        id: "previous",
        legendChinese: "上一轮轨迹",
        kind: "previous",
        lineStyle: 1,
        points: [
          { x: 1, y: 1 },
          { x: 2.5, y: 1 },
          { x: 4, y: 1.3 }
        ]
      },
      {
        id: "current",
        legendChinese: "当前轨迹",
        kind: "current",
        lineStyle: 0,
        points: [
          { x: 2, y: 1 },
          { x: 3.8, y: 1.2 },
          { x: 5.8, y: 1.9 }
        ]
      }
    ],
    dynamicMarkers: [
      { id: "vehicle", kind: "vehicle", labelChinese: "车辆", position: { x: 2, y: 1 } },
      { id: "plan-start", kind: "plan-start", labelChinese: "计划起点", position: { x: 2, y: 1 } },
      { id: "gear-switch-end", kind: "gear-switch-end", labelChinese: "换向点 1 / s_end", position: { x: 6, y: 2 } }
    ],
    charts: [
      chart("ls", "横向偏移", "ReferenceS (m)", "l (m)", [[0, 0.05], [2, 0.08], [4, 0.02]], [
        { id: "ls-s-end", kind: "s-end", labelChinese: "s_end", x: 4 }
      ]),
      chart("st", "时空轨迹", "t (s)", "PathS (m)", [[0, 0], [1, 1.2], [2, 3]], [
        { id: "st-s-end", kind: "s-end", labelChinese: "s_end", x: 2 }
      ])
    ],
    statusValues: [],
    cycleSummary: {
      cycleVersion: 1,
      occurredAtUtc: "2026-08-06T00:00:00Z",
      status: "成功",
      published: true,
      planningElapsedMilliseconds: 4.2,
      segmentIndex: 0,
      direction: "forward",
      longitudinalMode: "full",
      terminalType: "goal",
      terminalVelocity: 0,
      terminalAcceleration: 0,
      failureReason: ""
    }
  };
}

function fakeContext() {
  return {
    setTransform() {},
    clearRect() {},
    fillRect() {}
  };
}

function loadApp({ staticSnapshot = makeStaticSnapshot(), dynamicSnapshot = makeDynamicSnapshot() } = {}) {
  const dom = new JSDOM(readWebFile("index.html"), {
    runScripts: "outside-only",
    pretendToBeVisual: true,
    url: "http://127.0.0.1/observation?token=test"
  });
  const { window } = dom;
  window.devicePixelRatio = 1;
  window.__TRAJECTORY_VISUALIZATION_TEST__ = true;
  window.__fetchCalled = false;
  window.__eventSourceCreated = false;
  window.fetch = async () => {
    window.__fetchCalled = true;
    throw new Error("test mode must not fetch");
  };
  window.EventSource = class {
    constructor() {
      window.__eventSourceCreated = true;
    }

    close() {}
  };
  const style = window.document.createElement("style");
  style.textContent = readWebFile("app.css");
  window.document.head.append(style);
  window.HTMLCanvasElement.prototype.getContext = () => fakeContext();
  window.eval(readWebFile("app.js"));

  if (window.__trajectoryVisualizationTestHooks) {
    const hooks = window.__trajectoryVisualizationTestHooks;
    hooks.setBootstrap(staticSnapshot);
    hooks.receiveFrame({ snapshot: dynamicSnapshot, history: [] });
  }

  return dom;
}

function deepFreeze(value) {
  if (value && typeof value === "object" && !Object.isFrozen(value)) {
    Object.freeze(value);
    Object.values(value).forEach(deepFreeze);
  }
  return value;
}

function dispatchPointer(target, type, clientX, clientY, options = {}) {
  const window = target.ownerDocument.defaultView;
  const EventCtor = window.PointerEvent || window.MouseEvent;
  target.dispatchEvent(new EventCtor(type, {
    bubbles: true,
    cancelable: true,
    pointerId: 1,
    button: 0,
    clientX,
    clientY,
    ...options
  }));
}

function dispatchWheel(target, clientX, clientY, deltaY) {
  const window = target.ownerDocument.defaultView;
  const EventCtor = window.WheelEvent || window.MouseEvent;
  target.dispatchEvent(new EventCtor("wheel", {
    bubbles: true,
    cancelable: true,
    clientX,
    clientY,
    deltaY
  }));
}

function chartSnapshot(host) {
  return {
    ticks: [...host.querySelectorAll(".axis-tick-x")].map(tick => tick.textContent).join("|"),
    path: host.querySelector("path.chart-data")?.getAttribute("d") || ""
  };
}

test("tab switching hides inactive sections despite CSS grid rules", () => {
  const dom = loadApp();
  const { document, getComputedStyle } = dom.window;
  const overview = document.querySelector("#overview");
  const lsSt = document.querySelector("#ls-st");
  const kinematics = document.querySelector("#kinematics");

  assert.equal(overview.hidden, false);
  assert.notEqual(getComputedStyle(overview).display, "none");

  document.querySelector('button[data-tab="ls-st"]').click();

  assert.equal(overview.hidden, true);
  assert.equal(getComputedStyle(overview).display, "none");
  assert.equal(lsSt.hidden, false);
  assert.notEqual(getComputedStyle(lsSt).display, "none");
  assert.equal(kinematics.hidden, true);
  assert.equal(getComputedStyle(kinematics).display, "none");
});

test("ST chart renders y ticks, units, and s_end annotation", () => {
  const dom = loadApp();
  const { document } = dom.window;
  document.querySelector('button[data-tab="ls-st"]').click();

  const yTicks = document.querySelectorAll("#st .axis-tick-y");
  const xTicks = document.querySelectorAll("#st .axis-tick-x");
  const yLabel = document.querySelector("#st .axis-label-y");
  const xLabel = document.querySelector("#st .axis-label-x");

  assert.ok(yTicks.length >= 4, "ST renders y tick text");
  assert.ok(xTicks.length >= 4, "ST renders x tick text");
  assert.ok([...yTicks].every(tick => tick.textContent.length > 0), "y ticks have readable values");
  assert.equal(yLabel.textContent, "PathS (m)");
  assert.equal(xLabel.textContent, "t (s)");
  assert.ok(document.querySelector("#st .chart-annotation-label")?.textContent.includes("s_end"));
});

test("ST PathS polyline rises and semantic world layers are ordered", () => {
  const dom = loadApp();
  const { document } = dom.window;
  document.querySelector('button[data-tab="ls-st"]').click();
  const stPath = document.querySelector("#st path.chart-data");
  assert.ok(stPath, "ST chart has a polyline");
  const segments = stPath.getAttribute("d").split(/(?=[ML])/).filter(Boolean);
  const yValues = segments.map(segment => Number(segment.slice(1).trim().split(/[\s,]+/)[1]));
  assert.ok(yValues.length >= 3, "ST polyline has at least three rendered points");
  assert.ok(yValues[1] < yValues[0] && yValues[2] < yValues[1],
    "ST PathS increases upward in rendered pixel coordinates");

  document.querySelector('button[data-tab="overview"]').click();
  const overlay = document.querySelector("#world-overlay");
  const paths = [...overlay.querySelectorAll("path")];
  const layerClasses = ["world-coarse", "world-local-g2", "world-segment-active", "world-previous", "world-current"];
  const indices = layerClasses.map(className => paths.findIndex(path => path.classList.contains(className)));
  layerClasses.forEach((className, index) => assert.ok(indices[index] !== -1, className + " exists"));
  for (let index = 1; index < indices.length; index += 1) {
    assert.ok(indices[index - 1] < indices[index], "semantic world layer order is preserved");
  }
});

test("vehicle, plan start, gear switch, and final goal use semantic markers", () => {
  const dom = loadApp();
  const { document } = dom.window;
  const overlay = document.querySelector("#world-overlay");

  for (const className of ["marker-vehicle", "marker-plan-start", "marker-gear-switch", "marker-final-goal"]) {
    assert.ok(overlay.querySelector("." + className), className + " is rendered");
  }
  assert.equal(overlay.querySelectorAll(".marker-vehicle circle").length, 0,
    "vehicle marker is not a fixed pixel circle");
});

test("empty overview names the missing Local G2 source", () => {
  const dom = loadApp({
    staticSnapshot: makeStaticSnapshot({ empty: true }),
    dynamicSnapshot: makeDynamicSnapshot({ empty: true })
  });
  const emptyState = dom.window.document.querySelector("#world-overlay .world-empty-state");

  assert.ok(emptyState, "empty overview renders a visible state");
  assert.match(emptyState.textContent, /未收到.*Local G2 路径/);
});

test("test mode exposes frozen hooks without fetch or SSE", () => {
  const dom = loadApp();
  const { window } = dom;

  assert.equal(window.__fetchCalled, false);
  assert.equal(window.__eventSourceCreated, false);
  assert.ok(window.__trajectoryVisualizationTestHooks, "test hooks are exposed");
  assert.equal(Object.isFrozen(window.__trajectoryVisualizationTestHooks), true);
  for (const name of ["setBootstrap", "receiveFrame", "renderActiveTab", "installTabs"]) {
    assert.equal(typeof window.__trajectoryVisualizationTestHooks[name], "function", name + " hook exists");
  }
  assert.equal("state" in window.__trajectoryVisualizationTestHooks, false);
});

test("wheel and box zoom change ST ticks and path without mutating frozen snapshot", () => {
  const dynamicSnapshot = deepFreeze(makeDynamicSnapshot());
  const serializedBefore = JSON.stringify(dynamicSnapshot);
  const dom = loadApp({ dynamicSnapshot });
  const { document } = dom.window;
  document.querySelector('button[data-tab="ls-st"]').click();
  const st = document.querySelector("#st");
  const svg = st.querySelector("svg");
  const before = chartSnapshot(st);

  dispatchPointer(svg, "pointerdown", 400, 100);
  dispatchPointer(svg, "pointermove", 360, 80);
  dispatchPointer(svg, "pointerup", 360, 80);

  const afterBox = chartSnapshot(st);
  assert.notEqual(afterBox.ticks, before.ticks);
  assert.notEqual(afterBox.path, before.path);

  dispatchWheel(st.querySelector("svg"), 300, 100, -120);

  const afterWheel = chartSnapshot(st);
  assert.notEqual(afterWheel.ticks, afterBox.ticks);
  assert.notEqual(afterWheel.path, afterBox.path);
  assert.equal(JSON.stringify(dynamicSnapshot), serializedBefore);
});

test("box zoom ignores drags shorter than 4 CSS pixels", () => {
  const dom = loadApp();
  const { document } = dom.window;
  document.querySelector('button[data-tab="ls-st"]').click();
  const st = document.querySelector("#st");
  const svg = st.querySelector("svg");
  const before = chartSnapshot(st);

  dispatchPointer(svg, "pointerdown", 200, 100);
  dispatchPointer(svg, "pointermove", 202, 102);
  dispatchPointer(svg, "pointerup", 202, 102);

  assert.deepEqual(chartSnapshot(st), before);
});

test("reset restores the original chart data domain", () => {
  const dom = loadApp();
  const { document } = dom.window;
  document.querySelector('button[data-tab="ls-st"]').click();
  const st = document.querySelector("#st");
  const before = chartSnapshot(st);

  dispatchWheel(st.querySelector("svg"), 300, 100, -120);

  const zoomed = chartSnapshot(st);
  assert.notEqual(zoomed.ticks, before.ticks);

  st.querySelector('button[data-action="reset"]').click();
  assert.deepEqual(chartSnapshot(st), before);
});

test("fullscreen requests only the selected chart host", () => {
  const dom = loadApp();
  const { document, Element } = dom.window;
  const requested = [];
  Element.prototype.requestFullscreen = function requestFullscreen() {
    requested.push(this.id);
  };
  document.querySelector('button[data-tab="ls-st"]').click();
  const st = document.querySelector("#st");
  const ls = document.querySelector("#ls");

  st.querySelector('button[data-action="fullscreen"]').click();

  assert.deepEqual(requested, ["st"]);
  assert.equal(st.classList.contains("is-fullscreen"), true);
  assert.equal(ls.classList.contains("is-fullscreen"), false);
});

test("new SSE frame keeps an active viewport and leaves its snapshot unchanged", () => {
  const first = deepFreeze(makeDynamicSnapshot());
  const dom = loadApp({ dynamicSnapshot: first });
  const { document } = dom.window;
  document.querySelector('button[data-tab="ls-st"]').click();
  const st = document.querySelector("#st");
  dispatchWheel(st.querySelector("svg"), 300, 100, -120);
  const zoomed = chartSnapshot(st);

  const second = JSON.parse(JSON.stringify(first));
  second.sequence = 2;
  deepFreeze(second);
  const serializedSecond = JSON.stringify(second);
  dom.window.__trajectoryVisualizationTestHooks.receiveFrame({ snapshot: second, history: [] });

  assert.deepEqual(chartSnapshot(st), zoomed);
  assert.equal(JSON.stringify(second), serializedSecond);
});
