// IMP-90 (#90) u14 — vitest coverage for the pure helpers exported by
// `StructureEditOverlay`. The React component itself is not rendered
// (jsdom / @testing-library NOT in Front devDependencies — verified in
// `Front/package.json`); we test the deterministic pieces that drive its
// JSX: `resolveEffectiveSlotOrder` (effective-order resolution under
// override) and `moveItem` (immutable reorder primitive).
//
// Upstream / downstream contracts (verified by prior units):
//   - u2  KNOWN_AXES += structure_overrides (Python backend).
//   - u3  vite allowlist += structure_overrides.
//   - u6  structure_override_resolver — inner shape locked to
//         {slot_order, hidden_slots}; frame swap REJECTED to existing
//         frames axis.
//   - u10 typed-client `StructureOverridePerZone` + extract helper.
//   - u15 (next) will debounce + PUT the emitted capture.
//
// u14 scope: pure helpers only. React render path is verified by Codex
// auditor via static read of the JSX (no runtime test possible without
// jsdom). Tests below are intentionally side-effect-free.

import { describe, it, expect } from "vitest";
import {
  resolveEffectiveSlotOrder,
  moveItem,
} from "../src/components/StructureEditOverlay";

// ─────────────────────────────────────────────────────────────────────
// resolveEffectiveSlotOrder
// ─────────────────────────────────────────────────────────────────────

describe("resolveEffectiveSlotOrder — no override", () => {
  it("returns a fresh copy of the discovered keys when slotOrder is undefined", () => {
    const discovered = ["a", "b", "c"];
    const out = resolveEffectiveSlotOrder(discovered, undefined);
    expect(out).toEqual(["a", "b", "c"]);
    expect(out).not.toBe(discovered);
  });
  it("returns a fresh copy when slotOrder is null", () => {
    const out = resolveEffectiveSlotOrder(["a", "b"], null);
    expect(out).toEqual(["a", "b"]);
  });
  it("returns a fresh copy when slotOrder is empty []", () => {
    const out = resolveEffectiveSlotOrder(["a", "b"], []);
    expect(out).toEqual(["a", "b"]);
  });
  it("handles empty discovered list (no slots in zone)", () => {
    expect(resolveEffectiveSlotOrder([], undefined)).toEqual([]);
    expect(resolveEffectiveSlotOrder([], ["x"])).toEqual([]);
  });
});

describe("resolveEffectiveSlotOrder — full override", () => {
  it("reorders all discovered keys per slotOrder", () => {
    expect(
      resolveEffectiveSlotOrder(["a", "b", "c"], ["c", "a", "b"]),
    ).toEqual(["c", "a", "b"]);
  });
  it("is idempotent when slotOrder matches discovered order", () => {
    expect(
      resolveEffectiveSlotOrder(["a", "b", "c"], ["a", "b", "c"]),
    ).toEqual(["a", "b", "c"]);
  });
});

describe("resolveEffectiveSlotOrder — partial / drift override", () => {
  it("appends missing discovered keys in backend order at the tail", () => {
    // user reordered b -> first, but c was added later by backend.
    expect(
      resolveEffectiveSlotOrder(["a", "b", "c"], ["b", "a"]),
    ).toEqual(["b", "a", "c"]);
  });
  it("drops override entries that no longer exist in discovered keys", () => {
    // user had slot 'x' before; backend dropped it.
    expect(
      resolveEffectiveSlotOrder(["a", "b"], ["x", "a", "b"]),
    ).toEqual(["a", "b"]);
  });
  it("dedupes duplicate entries within slotOrder", () => {
    expect(
      resolveEffectiveSlotOrder(["a", "b", "c"], ["a", "a", "b"]),
    ).toEqual(["a", "b", "c"]);
  });
  it("dedupe + drop + append all together (stress)", () => {
    expect(
      resolveEffectiveSlotOrder(
        ["a", "b", "c", "d"],
        ["d", "x", "d", "a", "ghost"],
      ),
    ).toEqual(["d", "a", "b", "c"]);
  });
  it("ignores non-string entries in slotOrder", () => {
    const bogus = ["a", null as unknown as string, undefined as unknown as string, "b"];
    expect(resolveEffectiveSlotOrder(["a", "b"], bogus)).toEqual(["a", "b"]);
  });
});

// ─────────────────────────────────────────────────────────────────────
// moveItem
// ─────────────────────────────────────────────────────────────────────

describe("moveItem — happy paths", () => {
  it("moves index 0 down by 1 (swap with index 1)", () => {
    expect(moveItem(["a", "b", "c"], 0, 1)).toEqual(["b", "a", "c"]);
  });
  it("moves index 2 up by 1 (swap with index 1)", () => {
    expect(moveItem(["a", "b", "c"], 2, -1)).toEqual(["a", "c", "b"]);
  });
  it("moves across larger delta (swap with target)", () => {
    expect(moveItem(["a", "b", "c", "d"], 0, 2)).toEqual(["c", "b", "a", "d"]);
  });
});

describe("moveItem — bounds", () => {
  it("no-op (fresh copy) when moving first up", () => {
    const src = ["a", "b", "c"];
    const out = moveItem(src, 0, -1);
    expect(out).toEqual(["a", "b", "c"]);
    expect(out).not.toBe(src);
  });
  it("no-op when moving last down", () => {
    expect(moveItem(["a", "b", "c"], 2, 1)).toEqual(["a", "b", "c"]);
  });
  it("no-op when index negative", () => {
    expect(moveItem(["a", "b"], -1, 1)).toEqual(["a", "b"]);
  });
  it("no-op when index past end", () => {
    expect(moveItem(["a", "b"], 5, -1)).toEqual(["a", "b"]);
  });
  it("no-op when target falls out of range from large delta", () => {
    expect(moveItem(["a", "b", "c"], 1, 99)).toEqual(["a", "b", "c"]);
  });
  it("no-op on empty array (any index)", () => {
    expect(moveItem<string>([], 0, 1)).toEqual([]);
  });
});

describe("moveItem — immutability", () => {
  it("never mutates the input array", () => {
    const src = ["a", "b", "c"];
    moveItem(src, 0, 1);
    expect(src).toEqual(["a", "b", "c"]);
  });
  it("returns a new reference even when no-op", () => {
    const src = ["a", "b"];
    expect(moveItem(src, 0, -1)).not.toBe(src);
  });
  it("preserves T-typed values (number array)", () => {
    expect(moveItem([1, 2, 3], 0, 1)).toEqual([2, 1, 3]);
  });
});
