"use strict";

const test = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");

const root = path.resolve(__dirname, "..", "..");
const app = fs.readFileSync(path.join(root, "Web", "app.js"), "utf8");

function functionSource(name, nextName) {
  const start = app.indexOf(`function ${name}(`);
  const end = app.indexOf(`function ${nextName}(`, start + 1);
  assert.ok(start >= 0 && end > start, `${name} must precede ${nextName}`);
  return app.slice(start, end).trim();
}

function evaluateFunction(name, nextName, dependencies = {}) {
  const names = Object.keys(dependencies);
  return new Function(...names,
    `return (${functionSource(name, nextName)});`)(...names.map(key => dependencies[key]));
}

function stateStub() {
  return {
    namedPlaylist: { guardByEntryId: new Map() },
    manualFinancialRestore: { entries: new Map() }
  };
}

test("every restored workflow boundary blocks direct scene and DB-row edits", () => {
  const state = stateStub();
  const blockReason = evaluateFunction(
    "playlistEntryManualEditBlockReason",
    "normalizeManualSceneSelection",
    { state });
  const protectedEntries = [
    ["krx", "legacy-theme-workflow", { namedKrxRestore: {} }],
    ["nxt", "legacy-theme-workflow", { namedNxtRestore: {} }],
    ["foreign-stock", "legacy-named-foreign-stock-workflow", {}],
    ["foreign-index", "legacy-foreign-index-candle-workflow", {}],
    ["manual-financial", "manual-financial-workflow", {}],
    ["manual-lists", "legacy-manual-lists-workflow", {}]
  ];
  for (const [id, source, proof] of protectedEntries) {
    const entry = Object.freeze({
      id,
      enabled: true,
      operator: Object.freeze({ source, ...proof })
    });
    assert.equal(blockReason(entry), "trusted-workflow-entry", id);
  }

  const guarded = Object.freeze({ id: "guarded", enabled: true });
  state.namedPlaylist.guardByEntryId.set(guarded.id, Object.freeze({ trusted: false }));
  assert.equal(blockReason(guarded), "named-playlist-entry");
  const pending = Object.freeze({ id: "pending", enabled: true });
  state.manualFinancialRestore.entries.set(pending.id, Object.freeze({ status: "load" }));
  assert.equal(blockReason(pending), "manual-restore-entry");
});

test("a frozen ordinary entry receives scene values through an immutable replacement", () => {
  const state = stateStub();
  const blockReason = evaluateFunction(
    "playlistEntryManualEditBlockReason",
    "normalizeManualSceneSelection",
    { state });
  const normalize = evaluateFunction(
    "normalizeManualSceneSelection",
    "recreatePlaylistEntryWithSceneSelection");
  const sceneAliases = item => item.aliases;
  const recreate = evaluateFunction(
    "recreatePlaylistEntryWithSceneSelection",
    "recreatePlaylistEntryWithLiveSelection",
    {
      playlistEntryManualEditBlockReason: blockReason,
      normalizeManualSceneSelection: normalize,
      sceneAliases
    });
  const entry = Object.freeze({
    id: "ordinary",
    code: "5001",
    aliases: Object.freeze(["5001", "N5001"]),
    enabled: true,
    fadeDuration: 6,
    selection: Object.freeze({
      groupCode: "KOSPI",
      subject: "OLD",
      graphicType: "",
      subtype: "",
      dataCode: ""
    })
  });
  const replacement = recreate(entry, {
    groupCode: " NXT_KOSPI ",
    subject: " 삼성전자 ",
    graphicType: "CURRENT",
    subtype: "",
    dataCode: "005930"
  }, "N5001", 4);

  assert.notEqual(replacement, entry);
  assert.equal(entry.code, "5001");
  assert.equal(entry.fadeDuration, 6);
  assert.equal(entry.selection.subject, "OLD");
  assert.equal(replacement.code, "N5001");
  assert.equal(replacement.fadeDuration, 4);
  assert.equal(replacement.selection.groupCode, "NXT_KOSPI");
  assert.equal(replacement.selection.subject, "삼성전자");
  assert.equal(Object.isFrozen(replacement.selection), true);
  assert.equal(recreate(entry, replacement.selection, "invalid", 4), null);
});

test("live DB selection also replaces only an unprotected ordinary entry", () => {
  const state = stateStub();
  const blockReason = evaluateFunction(
    "playlistEntryManualEditBlockReason",
    "normalizeManualSceneSelection",
    { state });
  const normalize = evaluateFunction(
    "normalizeManualSceneSelection",
    "recreatePlaylistEntryWithSceneSelection");
  const recreate = evaluateFunction(
    "recreatePlaylistEntryWithLiveSelection",
    "replacePlaylistEntryAt",
    {
      playlistEntryManualEditBlockReason: blockReason,
      normalizeManualSceneSelection: normalize
    });
  const ordinary = Object.freeze({
    id: "ordinary-live",
    code: "5001",
    enabled: true,
    selection: Object.freeze({
      groupCode: "KOSPI_STOCK",
      subject: "OLD",
      graphicType: "CURRENT",
      subtype: "",
      dataCode: "000001"
    })
  });
  const selection = {
    groupCode: "KOSPI_STOCK",
    subject: "삼성전자",
    graphicType: "CURRENT",
    subtype: "",
    dataCode: "005930"
  };
  const replacement = recreate(ordinary, selection);
  assert.notEqual(replacement, ordinary);
  assert.equal(ordinary.selection.subject, "OLD");
  assert.equal(replacement.selection.subject, "삼성전자");
  assert.equal(replacement.subject, "삼성전자");
  assert.equal(replacement.dataCode, "005930");

  const protectedEntry = Object.freeze({
    ...ordinary,
    id: "protected-live",
    operator: Object.freeze({ source: "legacy-stock-workflow" })
  });
  assert.equal(recreate(protectedEntry, selection), null);
});

test("both edit handlers keep snapshot locking and contain no playlist-entry assignment", () => {
  const render = functionSource("renderSceneSelectionForm", "applySceneSelection");
  const scene = functionSource("applySceneSelection", "applyCutAliasPreset");
  const live = functionSource("applyLiveRowToCurrentCut", "renderLiveTables");

  assert.match(render, /playlistEntryManualEditBlockReason\(item\)/u);
  assert.match(scene, /if \(!requirePlaylistEditable\(\)\) return;/u);
  assert.match(scene, /playlistEntryManualEditBlockReason\(item\)/u);
  assert.match(scene, /recreatePlaylistEntryWithSceneSelection/u);
  assert.match(scene, /replacePlaylistEntryAt\(state\.currentIndex, replacement\)/u);
  assert.match(live, /if \(!requirePlaylistEditable\(\)\) return;/u);
  assert.match(live, /playlistEntryManualEditBlockReason\(item\)/u);
  assert.match(live, /recreatePlaylistEntryWithLiveSelection/u);
  assert.match(live, /replacePlaylistEntryAt\(state\.currentIndex, replacement\)/u);
  assert.doesNotMatch(app,
    /\bitem\.[A-Za-z_$][A-Za-z0-9_$]*\s*=(?!=)/u);
});
