// Rôle : teste les parseurs d'import texte utilisés par les outils toolbox.
import { test } from "node:test";
import assert from "node:assert/strict";
import { evaluateTableCell } from "../website/src/features/toolboxes/modules/tableFormulaEngine.js";
import { parseColonImportLines } from "../website/src/features/toolboxes/modules/textImport.js";
import { getTimePatternRecurrenceMs, getTimePatternTargetMs } from "../website/src/features/toolboxes/modules/timerUtils.js";
import { compactModuleDataForStorage, createToolboxExportPayload, normalizeChecklistData, normalizeCombosData, normalizeNotepadData, normalizeTableData, normalizeTaskPlannerData } from "../website/src/features/toolboxes/storage/toolboxStorage.js";
import { applyGroupedReorderOperation, completeGroupOrder, getBoundaryItemId, getGroupedEntries, moveGroupOrder, moveGroupOrderToEnd, moveGroupOrderToStart, moveItem, moveItemGroup } from "../website/src/hooks/useGroupedReorder.js";

test("colon text import keeps urls intact after the first separator", () => {
  assert.deepEqual(parseColonImportLines("Build:https://example.com/build?class=rogue\nPotion:10"), [
    { label: "Build", value: "https://example.com/build?class=rogue" },
    { label: "Potion", value: "10" }
  ]);
});

test("colon text import accepts labels without value", () => {
  assert.deepEqual(parseColonImportLines("Simple item"), [
    { label: "Simple item", value: "" }
  ]);
});

test("time pattern recurrence uses configured frequency instead of next remaining delay", () => {
  const now = new Date(2026, 0, 1, 13, 23, 45, 0).getTime();
  const target = getTimePatternTargetMs("X:24:X", now);
  assert.equal(new Date(target).getHours(), 13);
  assert.equal(new Date(target).getMinutes(), 24);
  assert.equal(new Date(target).getSeconds(), 0);
  assert.equal(getTimePatternRecurrenceMs("X:24:X"), 60 * 60 * 1000);
  assert.equal(getTimePatternRecurrenceMs("X:X:30"), 60 * 1000);
});

test("table formulas evaluate arithmetic, parentheses and cell references", () => {
  const cells = {
    A1: "100",
    A2: "20",
    B1: "=A1+A2*2",
    B2: "=(A1+A2)/2",
    C1: "=B1-B2/3"
  };
  const dimensions = { rows: 10, columns: 6 };

  assert.equal(evaluateTableCell("B1", cells, dimensions).display, "140");
  assert.equal(evaluateTableCell("B2", cells, dimensions).display, "60");
  assert.equal(evaluateTableCell("C1", cells, dimensions).display, "120");
});

test("table formulas report invalid references, division by zero and circular references", () => {
  const dimensions = { rows: 10, columns: 6 };

  assert.equal(evaluateTableCell("A1", { A1: "=Z99+1" }, dimensions).error, "reference");
  assert.equal(evaluateTableCell("A1", { A1: "=10/0" }, dimensions).error, "division");
  assert.equal(evaluateTableCell("A1", { A1: "=B1", B1: "=A1" }, dimensions).error, "cycle");
  assert.deepEqual(evaluateTableCell("A1", { A1: "texte", B1: "=A1+1" }, dimensions), { value: null, display: "texte", error: "" });
  assert.equal(evaluateTableCell("B1", { A1: "texte", B1: "=A1+1" }, dimensions).error, "reference");
});

test("table storage clamps dimensions and compacts non-empty cells", () => {
  const normalized = normalizeTableData({
    rows: 200,
    columns: 40,
    cells: {
      A1: "Boss ",
      T50: "=A1+1",
      U51: "Ignored",
      B1: " "
    },
    rowLabels: {
      0: "Boss final ",
      1: "2",
      50: "Ignored"
    },
    columnLabels: {
      0: "Item final ",
      1: "B",
      20: "Ignored"
    }
  });

  assert.equal(normalized.rows, 50);
  assert.equal(normalized.columns, 20);
  assert.deepEqual(normalized.cells, { A1: "Boss ", T50: "=A1+1" });
  assert.deepEqual(normalized.rowLabels, { 0: "Boss final " });
  assert.deepEqual(normalized.columnLabels, { 0: "Item final " });

  const compact = compactModuleDataForStorage("table", normalized);
  assert.equal(compact.rows, 50);
  assert.equal(compact.columns, 20);
  assert.deepEqual(compact.cells, { A1: "Boss ", T50: "=A1+1" });
  assert.deepEqual(compact.rowLabels, { 0: "Boss final " });
  assert.deepEqual(compact.columnLabels, { 0: "Item final " });
  assert.deepEqual(compactModuleDataForStorage("table", { rows: 10, columns: 6, cells: {}, columnLabels: { 0: "Item " } }), {
    cells: {},
    columnLabels: { 0: "Item " }
  });
  assert.equal(compactModuleDataForStorage("table", { rows: 10, columns: 6, cells: {} }), null);
});

test("grouped reorder helpers group flat items and complete group order", () => {
  const items = [
    { id: "a", category: "" },
    { id: "b", category: "Boss" },
    { id: "c", category: "Farm" },
    { id: "d", category: "Boss" }
  ];

  assert.deepEqual(completeGroupOrder(["Farm"], ["Boss", "Farm"]), ["Farm", "Boss"]);
  assert.deepEqual(getGroupedEntries(items, {
    groupOrder: ["Farm"],
    getItemGroup: (item) => item.category,
    groupIdKey: "category",
    groupItemKey: "items"
  }), [
    { type: "item", item: items[0] },
    { type: "group", group: { category: "Boss", items: [items[1], items[3]] } },
    { type: "group", group: { category: "Farm", items: [items[2]] } }
  ]);
});

test("grouped reorder helpers move items and grouped item blocks", () => {
  const items = [{ id: "a" }, { id: "b" }, { id: "c" }, { id: "d" }];

  assert.deepEqual(moveItem(items, "a", "c", "after").map((item) => item.id), ["b", "c", "a", "d"]);
  assert.deepEqual(moveItemGroup(items, ["b", "c"], "a", "before").map((item) => item.id), ["b", "c", "a", "d"]);
  assert.equal(getBoundaryItemId(items, ["b", "c"], "before"), "b");
  assert.equal(getBoundaryItemId(items, ["b", "c"], "after"), "c");
});

test("grouped reorder helpers reorder categories at targets and edges", () => {
  const groups = ["Neutral", "Advanced", "Punish"];

  assert.deepEqual(moveGroupOrder(["Neutral", "Advanced", "Punish"], groups, "Punish", "Neutral", "before"), ["Punish", "Neutral", "Advanced"]);
  assert.deepEqual(moveGroupOrderToStart(["Neutral", "Advanced", "Punish"], groups, "Punish"), ["Punish", "Neutral", "Advanced"]);
  assert.deepEqual(moveGroupOrderToEnd(["Neutral", "Advanced", "Punish"], groups, "Neutral"), ["Advanced", "Punish", "Neutral"]);
});

test("grouped reorder operation moves items in and out of categories", () => {
  const data = {
    categoryOrder: ["Boss"],
    collapsedCategories: ["Farm"],
    items: [
      { id: "a" },
      { id: "b", category: "Boss" },
      { id: "c", category: "Boss" },
      { id: "d", category: "Farm" }
    ]
  };
  const config = {
    itemsKey: "items",
    groupOrderKey: "categoryOrder",
    collapsedGroupsKey: "collapsedCategories",
    getItemGroup: (item) => item.category || "",
    setItemGroup: (item, category) => {
      const nextItem = { ...item };
      if (category) nextItem.category = category;
      else delete nextItem.category;
      return nextItem;
    }
  };

  const movedIn = applyGroupedReorderOperation(data, {
    ...config,
    operation: {
      type: "item",
      sourceId: "a",
      targetId: "Farm",
      targetType: "group",
      placement: "before",
      sourceGroup: "",
      targetGroup: "Farm",
      sourceParentId: "",
      targetParentId: ""
    }
  });

  assert.deepEqual(movedIn.items.map((item) => `${item.id}:${item.category || ""}`), ["b:Boss", "c:Boss", "a:Farm", "d:Farm"]);
  assert.deepEqual(movedIn.collapsedCategories, []);

  const movedOut = applyGroupedReorderOperation(movedIn, {
    ...config,
    operation: {
      type: "item",
      sourceId: "a",
      targetId: "Boss",
      targetType: "boundary",
      placement: "after",
      sourceGroup: "Farm",
      targetGroup: "Boss",
      sourceParentId: "",
      targetParentId: ""
    }
  });

  assert.deepEqual(movedOut.items.map((item) => `${item.id}:${item.category || ""}`), ["b:Boss", "c:Boss", "a:", "d:Farm"]);
});

test("grouped reorder operation moves whole categories", () => {
  const data = {
    categoryOrder: ["Boss", "Farm"],
    items: [
      { id: "a", category: "Boss" },
      { id: "b", category: "Boss" },
      { id: "c", category: "Farm" },
      { id: "d", category: "Farm" },
      { id: "e" }
    ]
  };
  const moved = applyGroupedReorderOperation(data, {
    operation: {
      type: "group",
      sourceId: "Boss",
      targetId: "Farm",
      targetType: "group",
      placement: "after",
      sourceGroup: "Boss",
      targetGroup: "Farm",
      sourceParentId: "",
      targetParentId: ""
    },
    groupOrderKey: "categoryOrder",
    getItemGroup: (item) => item.category || "",
    setItemGroup: (item) => item
  });

  assert.deepEqual(moved.items.map((item) => item.id), ["c", "d", "a", "b", "e"]);
  assert.deepEqual(moved.categoryOrder, ["Farm", "Boss"]);
});

test("checklist data drops empty titled sections", () => {
  const normalized = normalizeChecklistData({
    sections: [
      { id: "empty", title: "Empty", items: [] },
      { id: "filled", title: "Filled", items: [{ id: "item1", label: "Potion" }] }
    ]
  });

  assert.deepEqual(normalized.sections.map((section) => section.id), ["filled"]);

  const compact = compactModuleDataForStorage("checklist", {
    sections: [
      { id: "empty", title: "Empty", items: [] },
      { id: "filled", title: "Filled", items: [{ id: "item1", label: "Potion" }] }
    ]
  });

  assert.deepEqual(compact.sections.map((section) => section.id), ["filled"]);
});

test("combos storage normalizes devices, steps and compact export ids", () => {
  const longText = "x".repeat(120);
  const normalized = normalizeCombosData({
    device: "invalid",
    categoryOrder: [`${longText} `, "Missing", `${longText} `],
    collapsedCategories: [`${longText} `, "", `${longText} `],
    combos: [
      {
        id: "rootCombo",
        name: "Root",
        device: "switch",
        inputs: [[{ kind: "button", value: "triangle", hold: true }]]
      },
      {
        id: "combo1",
        device: "n64",
        category: `${longText} `,
        name: `${longText} `,
        inputs: [
          [
            { kind: "direction", value: "down" },
            { kind: "button", value: "cross", hold: false, holdMs: 2000.4 },
            { kind: "button", value: "circle", holdMs: 100 },
            { kind: "bad", value: "ignored" }
          ],
          [],
          [{ kind: "key", value: "space" }]
        ]
      },
      { id: "empty", name: " ", inputs: [] }
    ]
  });

  assert.equal(normalized.device, "playstation");
  assert.equal(normalized.combos.length, 2);
  assert.equal(normalized.combos[0].device, "switch");
  assert.equal(normalized.combos[0].inputs[0][0].hold, true);
  assert.equal(normalized.combos[1].category.length, 80);
  assert.equal(normalized.combos[1].name.length, 80);
  assert.equal("note" in normalized.combos[1], false);
  assert.deepEqual(normalized.categoryOrder, [longText.slice(0, 80)]);
  assert.deepEqual(normalized.collapsedCategories, [longText.slice(0, 80)]);
  assert.deepEqual(normalized.combos[1].inputs, [
    [
      { kind: "direction", value: "down" },
      { kind: "button", value: "cross", hold: true, holdMs: 2000 },
      { kind: "button", value: "circle" }
    ],
    [{ kind: "key", value: "space" }]
  ]);

  const compact = compactModuleDataForStorage("combos", { ...normalized, device: "xbox" });
  assert.equal(compact.device, "xbox");
  assert.deepEqual(compact.categoryOrder, [longText.slice(0, 80)]);
  assert.deepEqual(compact.collapsedCategories, [longText.slice(0, 80)]);
  assert.equal(compact.combos[0].id, "rootCombo");
  assert.equal(compact.combos[0].device, "switch");
  assert.equal(compact.combos[0].inputs[0][0].hold, true);
  assert.equal(compact.combos[1].device, "n64");
  assert.equal(compact.combos[1].id, "combo1");
  assert.equal(compact.combos[1].category.length, 80);
  assert.equal(compact.combos[1].inputs[0][1].holdMs, 2000);
  assert.equal(compact.combos[1].inputs[0][1].hold, true);

  const exported = createToolboxExportPayload(
    { id: "toolbox1", name: "Combos", modules: [{ id: "module1", type: "combos" }], updatedAt: "2026-01-01T00:00:00.000Z" },
    { "toolbox1:module1": compact }
  );
  assert.equal(exported.modules.m1.combos[0].id, "o1");
  assert.equal(exported.modules.m1.combos[0].device, "switch");
  assert.deepEqual(exported.modules.m1.categoryOrder, [longText.slice(0, 80)]);
  assert.deepEqual(exported.modules.m1.collapsedCategories, [longText.slice(0, 80)]);
  assert.equal(exported.modules.m1.combos[1].id, "o2");
  assert.equal(exported.modules.m1.combos[1].device, "n64");
});

test("notepad data migrates text, sanitizes html and compacts drawings", () => {
  const migrated = normalizeNotepadData({ text: "Boss\nPhase 2" });
  assert.equal(migrated.text, "Boss\nPhase 2");
  assert.match(migrated.html, /<p>Boss<\/p><p>Phase 2<\/p>/);

  const normalized = normalizeNotepadData({
    html: '<h3 onclick="bad()">Titre</h3><script>alert(1)</script><span style="color: rgb(246, 196, 83); position: fixed">Important</span><img src=x>',
    updatedAt: "2026-07-29T12:00:00.000Z",
    drawingMode: "permanent",
    drawings: {
      strokes: [
        {
          id: "stroke1",
          color: "#f6c453",
          width: 40,
          points: [{ x: 1.123 }, { x: 4, y: 8 }, { x: 12.345, y: 16.789 }]
        }
      ]
    }
  });

  assert.match(normalized.html, /<h3>Titre<\/h3>/);
  assert.match(normalized.html, /<span style="color: #f6c453">Important<\/span>/);
  assert.doesNotMatch(normalized.html, /script|onclick|position|img/);
  assert.equal(normalized.updatedAt, "2026-07-29T12:00:00.000Z");
  assert.equal(normalized.drawings.strokes[0].width, 24);
  assert.deepEqual(normalized.drawings.strokes[0].points, [{ x: 4, y: 8 }, { x: 12.35, y: 16.79 }]);

  const compactTemporary = compactModuleDataForStorage("notepad", {
    html: "<p>Note</p>",
    text: "Note",
    drawingMode: "temporary",
    drawings: { strokes: normalized.drawings.strokes }
  });
  assert.equal(compactTemporary.drawings, undefined);
  assert.equal(compactTemporary.drawingMode, undefined);

  const compactPermanent = compactModuleDataForStorage("notepad", {
    html: "<p>Note</p>",
    text: "Note",
    updatedAt: "not a date",
    drawingMode: "permanent",
    drawings: normalized.drawings
  });
  assert.equal(compactPermanent.updatedAt, undefined);
  assert.equal(compactPermanent.drawingMode, "permanent");
  assert.equal(compactPermanent.drawings.strokes.length, 1);
});

test("image annotation storage keeps drawing strokes as percent coordinates", () => {
  const compact = compactModuleDataForStorage("imageAnnotation", {
    image: "data:image/png;base64,aaa",
    markers: [{ id: "marker1", x: 120, y: 50, label: "Boss" }],
    drawings: {
      strokes: [
        {
          id: "stroke1",
          color: "#22d3ee",
          width: 4,
          points: [{ x: 10.123, y: 20.456 }, { x: 140, y: -4 }]
        }
      ]
    }
  });

  assert.equal(compact.markers[0].x, 100);
  assert.deepEqual(compact.drawings.strokes[0].points, [{ x: 10.12, y: 20.46 }, { x: 100, y: 0 }]);

  const exported = createToolboxExportPayload(
    { id: "toolbox1", name: "Map", modules: [{ id: "module1", type: "imageAnnotation" }], updatedAt: "2026-01-01T00:00:00.000Z" },
    { "toolbox1:module1": compact }
  );
  assert.equal(exported.modules.m1.markers[0].id, "k1");
  assert.equal(exported.modules.m1.drawings.strokes[0].id, "d1");
});

test("task planner data normalizes invalid settings and relations", () => {
  const normalized = normalizeTaskPlannerData({
    weeklyResetDay: 9,
    resetTime: "27:80",
    hideCompleted: true,
    categoryOrder: ["Raid", "Missing", "Farm", "Raid"],
    collapsedCategories: ["Raid", "", "Raid"],
    tasks: [
      { id: "task1", title: "Daily", type: "daily", checked: true, checkedAt: 1000, category: "Farm" },
      { id: "task2", title: "Weekly", type: "weekly", weeklyResetDay: 4, category: "Raid" },
      { id: "task3", title: "Invalid type", type: "later", category: "Farm" }
    ],
    relations: [
      { id: "link1", fromTaskId: "task1", toTaskId: "task2" },
      { id: "dep1", fromTaskId: "task3", toTaskId: "task2", prerequisite: true },
      { id: "bad", fromTaskId: "task1", toTaskId: "missing" },
      { id: "self", fromTaskId: "task2", toTaskId: "task2" }
    ]
  });

  assert.equal(normalized.weeklyResetDay, 1);
  assert.equal(normalized.resetTime, "00:00");
  assert.equal(normalized.hideCompleted, true);
  assert.deepEqual(normalized.categoryOrder, ["Raid"]);
  assert.deepEqual(normalized.collapsedCategories, ["Raid"]);
  assert.equal(normalized.tasks[0].category, undefined);
  assert.equal(normalized.tasks[1].weeklyResetDay, 4);
  assert.equal(normalized.tasks[1].category, "Raid");
  assert.equal(normalized.tasks[2].type, "unique");
  assert.deepEqual(normalized.relations.map((relation) => relation.id), ["link1", "dep1"]);
  assert.equal(normalized.relations[0].prerequisite, false);
  assert.equal(normalized.relations[1].prerequisite, true);
});

test("task planner storage compacts defaults and export remaps task relations", () => {
  const compact = compactModuleDataForStorage("taskPlanner", {
    weeklyResetDay: 1,
    resetTime: "00:00",
    hideCompleted: true,
    collapsedCategories: ["Raid"],
    tasks: [
      { id: "task1", title: "Daily", description: "", type: "daily", checked: false },
      { id: "task2", title: "Weekly", description: "Run", type: "weekly", checked: true, checkedAt: 2000, weeklyResetDay: 5, category: "Raid" }
    ],
    categoryOrder: ["Raid"],
    relations: [
      { id: "dep1", fromTaskId: "task1", toTaskId: "task2", prerequisite: true }
    ]
  });

  assert.equal(compact.weeklyResetDay, undefined);
  assert.equal(compact.resetTime, undefined);
  assert.equal(compact.hideCompleted, true);
  assert.deepEqual(compact.collapsedCategories, ["Raid"]);
  assert.deepEqual(compact.categoryOrder, ["Raid"]);
  assert.deepEqual(compact.tasks[0], { id: "task1", title: "Daily", type: "daily" });
  assert.equal(compact.tasks[1].category, "Raid");
  assert.equal(compact.tasks[1].weeklyResetDay, 5);

  const exported = createToolboxExportPayload(
    { id: "toolbox1", name: "Planner", modules: [{ id: "module1", type: "taskPlanner" }], updatedAt: "2026-01-01T00:00:00.000Z" },
    { "toolbox1:module1": compact }
  );
  const exportedPlanner = exported.modules.m1;
  assert.deepEqual(exportedPlanner.tasks.map((task) => task.id), ["a1", "a2"]);
  assert.equal(exportedPlanner.relations[0].fromTaskId, "a1");
  assert.equal(exportedPlanner.relations[0].toTaskId, "a2");
  assert.equal(exportedPlanner.relations[0].prerequisite, true);
});
