import test from 'node:test';
import assert from 'node:assert/strict';
import { z } from 'zod';
import { compileGuided, encodeLabel, nextId, parseProcessJson, processExamples, processSchema, restoreGuided, toSourceMode, validateProcess, validateSource, type ProcessState } from '../../src/tools/mapper/core';

function guided() { const state = structuredClone(processExamples.onboarding); assert.equal(state.mode, 'guided'); return state; }

test('all four business examples validate and compile deterministically', () => {
  for (const state of Object.values(processExamples)) {
    const checked = validateProcess(state); assert.equal(checked.ok, true); assert.deepEqual(checked.warnings, []);
    assert.equal(compileGuided(state.graph), compileGuided(structuredClone(state.graph)));
    assert.deepEqual(validateSource(compileGuided(state.graph)), []);
  }
});

test('business-process loops are accepted without planner dependency rules', () => {
  const state = guided(); state.graph.edges.push({ id: 'back', from: 'welcome', to: 'enquiry', label: 'Return next year' });
  assert.equal(validateProcess(state).ok, true);
});

test('duplicate IDs, dangling connections and blank step names are rejected', () => {
  const duplicate = guided(); duplicate.graph.nodes[1].id = duplicate.graph.nodes[0].id; assert.equal(validateProcess(duplicate).ok, false);
  const dangling = guided(); dangling.graph.edges[0].to = 'absent'; assert.equal(validateProcess(dangling).ok, false);
  const blank = guided(); blank.graph.nodes[0].label = '   '; assert.equal(validateProcess(blank).ok, false);
});

test('separate groups and isolated steps produce useful warnings, not false invalidity', () => {
  const state = guided(); state.graph.edges = state.graph.edges.filter(edge => edge.from !== 'enquiry' && edge.to !== 'enquiry');
  const result = validateProcess(state); assert.equal(result.ok, true); assert.ok(result.warnings.some(issue => issue.message.includes('no connections'))); assert.ok(result.warnings.some(issue => issue.message.includes('separate groups')));
});

test('labels cannot add Mermaid statements, HTML, styling or links', () => {
  const state = guided(); const label = 'Cost "£50" & Québec [trial] #1;\n"] --> evil["<img onerror=alert(1)>`|\\';
  state.graph.nodes[0].label = label;
  const source = compileGuided(state.graph);
  assert.equal(source.split('\n').length, 1 + state.graph.nodes.length + state.graph.edges.length);
  assert.ok(source.includes('#34;')); assert.ok(source.includes('#10;')); assert.ok(source.includes('#60;')); assert.ok(!source.includes('<img'));
  assert.deepEqual(validateSource(source), []); assert.equal(state.graph.nodes[0].label, label);
});

test('encoding keeps accented business text and escapes the entity delimiter exactly once', () => {
  assert.equal(encodeLabel('Québec € £ –'), 'Québec € £ –');
  assert.equal(encodeLabel('#quot;'), '#35;quot#59;');
  assert.equal(encodeLabel('A&B'), 'A#38;B');
});

test('editing source preserves a recoverable graph without pretending to reverse parse', () => {
  const original = guided(); const source = toSourceMode(original); assert.equal(source.mode, 'source');
  if (source.mode !== 'source') throw new Error('Expected source mode');
  source.source = 'sequenceDiagram\n  Customer->>Team: A different process';
  assert.deepEqual(restoreGuided(source), original); assert.notEqual(source.source, compileGuided(original.graph));
  source.preservedGraph!.nodes[0].label = 'Changed copy'; assert.notEqual(source.preservedGraph!.nodes[0].label, original.graph.nodes[0].label);
});

test('source-only imports without a graph do not fabricate guided data', () => {
  const state: ProcessState = { schemaVersion: 1, tool: 'process-mapper', mode: 'source', source: 'sequenceDiagram\n  Client->>Office: Book a visit', preservedGraph: null };
  assert.equal(validateProcess(state).ok, true); assert.equal(restoreGuided(state), null);
});

test('JSON exports reopen losslessly in both editing modes', () => {
  for (const state of [guided(), toSourceMode(guided())]) {
    const parsed = parseProcessJson(JSON.stringify(state)); assert.equal(parsed.ok, true);
    if (parsed.ok) assert.deepEqual(parsed.value, state);
  }
});

test('unknown versions, extra configuration and prototype-pollution payloads are rejected', () => {
  assert.equal(validateProcess({ ...guided(), schemaVersion: 2 }).ok, false);
  assert.equal(validateProcess({ ...guided(), mermaid: { securityLevel: 'loose' } }).ok, false);
  for (const key of ['__proto__', 'constructor', 'prototype']) {
    const payload = JSON.parse(JSON.stringify(guided())); Object.defineProperty(payload.graph, key, { value: { polluted: true }, enumerable: true });
    assert.equal(validateProcess(payload).ok, false);
  }
  assert.equal(({} as Record<string, unknown>).polluted, undefined);
});

test('malformed, oversized, cyclic and non-plain inputs fail without mutation', () => {
  assert.equal(parseProcessJson('{').ok, false); assert.equal(parseProcessJson(' '.repeat(150_001)).ok, false);
  const original = guided(); const copy = structuredClone(original); const cyclic: Record<string, unknown> = {}; cyclic.self = cyclic;
  assert.equal(validateProcess(cyclic).ok, false); assert.equal(validateProcess(new Date()).ok, false); assert.deepEqual(original, copy);
});

test('safe flowchart shapes and labelled arrows are supported', () => {
  const source = 'flowchart LR\n  A(["Start"])\n  B{"Approved?"}\n  C[["System"]]\n  D[("Records")]\n  A --> B\n  B -->|"Yes"| C\n  C -.-> D';
  assert.deepEqual(validateSource(source), []);
});

test('sequence participants, notes, loops and messages are supported', () => {
  const source = 'sequenceDiagram\n  participant Client as Customer\n  actor Agent\n  autonumber\n  loop Until approved\n  Client->>Agent: Request help\n  activate Agent\n  Note right of Agent: Review the request\n  Agent-->>Client: Confirm next steps\n  deactivate Agent\n  end';
  assert.deepEqual(validateSource(source), []);
});

test('source-mode security boundary rejects renderer configuration and executable features', () => {
  const payloads = ['%%{init: {"securityLevel":"loose"}}%%\nflowchart LR\n  A["A"]', '---\nconfig:\n securityLevel: loose\n---\nflowchart LR\nA["A"]', 'flowchart LR\n  A["<img src=x>"]', 'flowchart LR\n  A["A"]\n  click A "https://example.com"', 'flowchart LR\n  A["A"]\n  classDef x fill:red', 'flowchart LR\n  A@{ img: "https://example.com/a.png" }', 'architecture-beta\n  service a(server)[A]', 'sequenceDiagram\n  A->>B: <svg onload=alert(1)>'];
  payloads.forEach(source => assert.notDeepEqual(validateSource(source), [], source));
});

test('source limits reject oversized UTF-8 and excessive nodes/connections', () => {
  assert.notDeepEqual(validateSource(`flowchart LR\nA["${'é'.repeat(16_000)}"]`), []);
  assert.notDeepEqual(validateSource(`flowchart LR\n${Array.from({ length: 101 }, (_, index) => `n${index}["Node"]`).join('\n')}`), []);
  assert.notDeepEqual(validateSource(`flowchart LR\nA["A"]\nB["B"]\n${'A --> B\n'.repeat(201)}`), []);
});

test('100 nodes and 200 edges remain an accepted bounded case', () => {
  const state = guided();
  state.graph.nodes = Array.from({ length: 100 }, (_, index) => ({ id: `n${index}`, label: `Step ${index + 1}`, type: 'process', team: '', description: '' }));
  state.graph.edges = Array.from({ length: 200 }, (_, index) => ({ id: `e${index}`, from: `n${index % 100}`, to: `n${(index + 1) % 100}`, label: '' }));
  assert.equal(validateProcess(state).ok, true);
});

test('generated IDs remain stable and collision-free after deletion', () => {
  assert.equal(nextId('step', ['step_1', 'step_3']), 'step_2'); assert.equal(nextId('link', ['link_1', 'link_2']), 'link_3');
});

test('a publishable JSON Schema identifies both modes and rejects extra keys', () => {
  const schema = z.toJSONSchema(processSchema); assert.ok(schema.oneOf); assert.equal(schema.oneOf.length, 2);
  schema.oneOf.forEach(branch => assert.equal(branch.additionalProperties, false));
});
