import assert from 'node:assert/strict';
import test from 'node:test';

import type { Project, ProjectNpc, ProjectNpcGroup, ProjectNpcRelation, Scene, SceneGraphEdge, SceneGraphNode } from '../types';
import {
  asAssetId,
  asGraphNodeId,
  asNpcGroupId,
  asNpcId,
  asNpcRelationId,
  asProjectId,
  asSceneId,
  asSceneNpcTokenId,
} from '../types/ids';
import { PROJECT_SCHEMA_VERSION } from '../types';

import {
  buildPartialExportProject,
  collectStorylineGraphNodeIds,
  computeGraphImportOffsetX,
  findSceneTitleConflicts,
  listExportedNpcsFromBundle,
  listImportableStorylines,
  mergeStorylinesIntoProject,
  newExportBundleProjectId,
  selectNpcsByIds,
} from './storylineExportImport';

const LABELS = { main: 'Основная линия', untitled: 'Без названия' };

function scene(id: string, title: string): Scene {
  return {
    id: asSceneId(id),
    title,
    description: '',
    previewAssetId: null,
    previewAssetType: null,
    previewThumbAssetId: null,
    previewVideoAutostart: false,
    previewRotationDeg: 0,
    darkenScene: false,
    traps: [],
    tokens: [],
    npcTokens: [],
    grid: { enabled: false, type: 'square', sizeN: 0.06, color: '#ffffff' },
    media: { videos: [], audios: [] },
    settings: { autoplayVideo: false, autoplayAudio: false, loopVideo: false, loopAudio: false },
    connections: [],
    layout: { x: 0, y: 0 },
  };
}

function node(id: string, sceneId: string, opts?: Partial<SceneGraphNode>): SceneGraphNode {
  return {
    id: asGraphNodeId(id),
    sceneId: asSceneId(sceneId),
    x: 0,
    y: 0,
    isStartScene: false,
    isSideStoryStart: false,
    sideStoryLineTitle: '',
    ...opts,
  };
}

function edge(id: string, src: string, tgt: string): SceneGraphEdge {
  return { id, sourceGraphNodeId: asGraphNodeId(src), targetGraphNodeId: asGraphNodeId(tgt) };
}

function minimalProject(overrides: Partial<Project> = {}): Project {
  return {
    id: asProjectId('p1'),
    meta: {
      name: 'Test',
      fileBaseName: 'test',
      createdAt: '2020-01-01T00:00:00.000Z',
      updatedAt: '2020-01-01T00:00:00.000Z',
      createdWithAppVersion: '1',
      appVersion: '1',
      schemaVersion: PROJECT_SCHEMA_VERSION,
    },
    scenes: {},
    sceneListOrder: [],
    assets: {},
    campaignAudios: [],
    materials: [],
    npcs: [],
    npcGroups: [],
    npcRelations: [],
    currentSceneId: null,
    currentGraphNodeId: null,
    sceneGraphNodes: [],
    sceneGraphEdges: [],
    ...overrides,
  };
}

void test('collectStorylineGraphNodeIds: main line excludes orphan and side nodes', () => {
  const project = minimalProject({
    scenes: {
      [asSceneId('s1')]: scene('s1', 'Start'),
      [asSceneId('s2')]: scene('s2', 'A'),
      [asSceneId('s3')]: scene('s3', 'Side'),
      [asSceneId('s4')]: scene('s4', 'Orphan'),
    },
    sceneGraphNodes: [
      node('main', 's1', { isStartScene: true, x: 0 }),
      node('a', 's2', { x: 100 }),
      node('side', 's3', { isSideStoryStart: true, x: 200 }),
      node('orphan', 's4', { x: 300 }),
    ],
    sceneGraphEdges: [edge('e1', 'main', 'a'), edge('e2', 'side', 'orphan')],
  });

  const mainIds = collectStorylineGraphNodeIds(project, { kind: 'main' });
  assert.deepEqual([...mainIds].sort(), ['a', 'main'].sort());
});

void test('buildPartialExportProject: subset, manifest, no free nodes', () => {
  const source = minimalProject({
    scenes: {
      [asSceneId('s1')]: scene('s1', 'Start'),
      [asSceneId('s2')]: scene('s2', 'A'),
      [asSceneId('s3')]: scene('s3', 'Side'),
      [asSceneId('s4')]: scene('s4', 'Orphan'),
    },
    sceneGraphNodes: [
      node('main', 's1', { isStartScene: true }),
      node('a', 's2'),
      node('side', 's3', { isSideStoryStart: true, sideStoryLineTitle: 'Квест' }),
      node('orphan', 's4'),
    ],
    sceneGraphEdges: [edge('e1', 'main', 'a'), edge('e2', 'side', 'orphan')],
  });

  const partial = buildPartialExportProject(
    source,
    [{ kind: 'side', startGraphNodeId: asGraphNodeId('side') }],
    { newProjectId: newExportBundleProjectId(), exportTitle: 'Export', labels: LABELS },
  );

  assert.equal(partial.sceneGraphNodes.length, 2);
  assert.ok(partial.sceneGraphNodes.some((n) => n.id === asGraphNodeId('side')));
  assert.ok(partial.sceneGraphNodes.some((n) => n.id === asGraphNodeId('orphan')));
  assert.equal(Object.keys(partial.scenes).length, 2);
  assert.ok(partial.scenes[asSceneId('s3')]);
  assert.ok(!partial.scenes[asSceneId('s1')]);
  assert.ok(!partial.scenes[asSceneId('s2')]);
  assert.equal(partial.exportedStorylines?.length, 1);
  assert.equal(partial.exportedStorylines![0]!.label, 'Квест');
  assert.equal(partial.exportedStorylines![0]!.kind, 'side');
});

void test('listImportableStorylines: disables main when target already has start', () => {
  const source = minimalProject({
    exportedStorylines: [
      { kind: 'main', startGraphNodeId: null, label: 'Основная линия' },
      { kind: 'side', startGraphNodeId: asGraphNodeId('side'), label: 'Побочная' },
    ],
  });

  const items = listImportableStorylines(source, LABELS, true);
  assert.equal(items[0]!.disabled, true);
  assert.equal(items[0]!.disabledReason, 'main_exists');
  assert.ok(!items[1]!.disabled);
});

void test('findSceneTitleConflicts: case-insensitive trimmed match', () => {
  const target = minimalProject({
    scenes: { [asSceneId('t1')]: scene('t1', ' Tavern ') },
  });
  const source = minimalProject({
    scenes: { [asSceneId('s1')]: scene('s1', 'tavern') },
  });

  const conflicts = findSceneTitleConflicts(target, source, [asSceneId('s1')]);
  assert.equal(conflicts.length, 1);
  assert.equal(conflicts[0]!.sourceTitle, 'tavern');
  assert.equal(conflicts[0]!.matches[0]!.sceneId, asSceneId('t1'));
});

void test('mergeStorylinesIntoProject: offset X, create scene, rename side title', () => {
  const target = minimalProject({
    scenes: { [asSceneId('t1')]: scene('t1', 'Existing') },
    sceneGraphNodes: [node('tgn', 't1', { x: 400 })],
  });

  const source = minimalProject({
    scenes: {
      [asSceneId('s1')]: scene('s1', 'New scene'),
      [asSceneId('s2')]: scene('s2', 'Side scene'),
    },
    sceneGraphNodes: [
      node('side', 's2', { isSideStoryStart: true, sideStoryLineTitle: 'Квест', x: 50, y: 10 }),
      node('a', 's1', { x: 150, y: 10 }),
    ],
    sceneGraphEdges: [edge('e1', 'side', 'a')],
  });

  target.sceneGraphNodes.push(
    node('existingSide', 't1', { isSideStoryStart: true, sideStoryLineTitle: 'Квест' }),
  );

  const offsetX = computeGraphImportOffsetX(target);
  const { project: merged, report } = mergeStorylinesIntoProject(
    target,
    source,
    [{ kind: 'side', startGraphNodeId: asGraphNodeId('side') }],
    [{ sourceSceneId: asSceneId('s1'), mode: 'create' }, { sourceSceneId: asSceneId('s2'), mode: 'create' }],
    { graphOffsetX: offsetX },
  );

  const imported = merged.sceneGraphNodes.filter((n) => n.id !== asGraphNodeId('tgn') && n.id !== asGraphNodeId('existingSide'));
  assert.equal(imported.length, 2);
  assert.equal(imported[0]!.x, offsetX);
  assert.equal(imported[1]!.x, offsetX + 100);

  const sideStart = imported.find((n) => n.isSideStoryStart);
  assert.equal(sideStart?.sideStoryLineTitle, 'Квест (2)');
  assert.equal(report.renamedSideTitles.length, 1);
  assert.equal(report.scenesCreated, 2);
  assert.equal(report.graphNodesAdded, 2);
  assert.equal(report.edgesAdded, 1);
});

void test('selectNpcsByIds / buildPartialExportProject: explicit NPCs, relations, ancestor groups', () => {
  const gRoot = asNpcGroupId('g_root');
  const gChild = asNpcGroupId('g_child');
  const n1 = asNpcId('npc1');
  const n2 = asNpcId('npc2');
  const n3 = asNpcId('npc3');
  const avatar = asAssetId('a1');
  const groups: ProjectNpcGroup[] = [
    { id: gRoot, name: 'Root', color: '#ffffff', parentId: null },
    { id: gChild, name: 'Child', color: '#ff0000', parentId: gRoot },
    { id: asNpcGroupId('g_other'), name: 'Other', color: '#00ff00', parentId: null },
  ];
  const npcs: ProjectNpc[] = [
    {
      id: n1,
      name: 'A',
      avatarAssetId: avatar,
      description: '',
      x: 0,
      y: 0,
      groupId: gChild,
      ringColor: '#c9a227',
      disposition: 'neutral',
      imageOffset: { x: 0, y: 0 },
      imageScale: 1,
    },
    {
      id: n2,
      name: 'B',
      avatarAssetId: avatar,
      description: '',
      x: 10,
      y: 0,
      groupId: gChild,
      ringColor: '#c9a227',
      disposition: 'neutral',
      imageOffset: { x: 0, y: 0 },
      imageScale: 1,
    },
    {
      id: n3,
      name: 'C',
      avatarAssetId: avatar,
      description: '',
      x: 20,
      y: 0,
      groupId: asNpcGroupId('g_other'),
      ringColor: '#c9a227',
      disposition: 'neutral',
      imageOffset: { x: 0, y: 0 },
      imageScale: 1,
    },
  ];
  const relations: ProjectNpcRelation[] = [
    { id: asNpcRelationId('r1'), sourceNpcId: n1, targetNpcId: n2, label: 'friends' },
    { id: asNpcRelationId('r2'), sourceNpcId: n1, targetNpcId: n3, label: 'rivals' },
  ];
  const source = minimalProject({
    scenes: { [asSceneId('s1')]: scene('s1', 'Start') },
    sceneGraphNodes: [node('main', 's1', { isStartScene: true })],
    assets: {
      [avatar]: {
        id: avatar,
        type: 'image',
        mime: 'image/png',
        originalName: 'a.png',
        relPath: 'assets/a.png',
        sha256: 'abc',
        sizeBytes: 1,
        createdAt: '2020-01-01T00:00:00.000Z',
      },
    },
    npcs,
    npcGroups: groups,
    npcRelations: relations,
  });

  assert.deepEqual(
    selectNpcsByIds(source, [n1, n2]).map((n) => n.id),
    [n1, n2],
  );

  const partial = buildPartialExportProject(source, [{ kind: 'main' }], {
    newProjectId: newExportBundleProjectId(),
    exportTitle: 'Export',
    labels: LABELS,
    npcIds: [n1, n2],
  });
  assert.equal(partial.npcs.length, 2);
  assert.ok(partial.npcs.every((n) => n.id === n1 || n.id === n2));
  assert.equal(partial.npcRelations.length, 1);
  assert.equal(partial.npcRelations[0]!.label, 'friends');
  assert.deepEqual(
    [...partial.npcGroups.map((g) => g.id)].sort(),
    [gRoot, gChild].sort(),
  );
  assert.ok(!partial.npcGroups.some((g) => g.id === asNpcGroupId('g_other')));
});

void test('buildPartialExportProject: keeps npcTokens only for exported NPCs', () => {
  const n1 = asNpcId('npc1');
  const n2 = asNpcId('npc2');
  const avatar = asAssetId('a1');
  const sceneId = asSceneId('s1');
  const source = minimalProject({
    scenes: {
      [sceneId]: {
        ...scene('s1', 'Start'),
        npcTokens: [
          { id: asSceneNpcTokenId('nt1'), npcId: n1, nx: 0.2, ny: 0.3, sizeN: 0.1, disposition: 'neutral' },
          { id: asSceneNpcTokenId('nt2'), npcId: n2, nx: 0.5, ny: 0.5, sizeN: 0.1, disposition: 'hostile' },
          { id: asSceneNpcTokenId('nt3'), npcId: asNpcId('missing'), nx: 0.1, ny: 0.1, sizeN: 0.1, disposition: 'neutral' },
        ],
      },
    },
    sceneGraphNodes: [node('main', 's1', { isStartScene: true })],
    assets: {
      [avatar]: {
        id: avatar,
        type: 'image',
        mime: 'image/png',
        originalName: 'a.png',
        relPath: 'assets/a.png',
        sha256: 'abc',
        sizeBytes: 1,
        createdAt: '2020-01-01T00:00:00.000Z',
      },
    },
    npcs: [
      {
        id: n1,
        name: 'A',
        avatarAssetId: avatar,
        description: '',
        x: 0,
        y: 0,
        groupId: null,
        ringColor: '#c9a227',
        disposition: 'neutral',
        imageOffset: { x: 0, y: 0 },
      imageScale: 1,
    },
      {
        id: n2,
        name: 'B',
        avatarAssetId: avatar,
        description: '',
        x: 10,
        y: 0,
        groupId: null,
        ringColor: '#c9a227',
        disposition: 'neutral',
        imageOffset: { x: 0, y: 0 },
      imageScale: 1,
    },
    ],
  });

  const partial = buildPartialExportProject(source, [{ kind: 'main' }], {
    newProjectId: newExportBundleProjectId(),
    exportTitle: 'Export',
    labels: LABELS,
    npcIds: [n1],
  });
  const tokens = partial.scenes[sceneId]?.npcTokens ?? [];
  assert.equal(tokens.length, 1);
  assert.equal(tokens[0]!.npcId, n1);
});

void test('mergeStorylinesIntoProject: remaps npcTokens to created NPC ids', () => {
  const avatar = asAssetId('a1');
  const sourceNpcId = asNpcId('src_npc');
  const source = minimalProject({
    scenes: {
      [asSceneId('s1')]: {
        ...scene('s1', 'Side'),
        npcTokens: [
          { id: asSceneNpcTokenId('nt1'), npcId: sourceNpcId, nx: 0.4, ny: 0.6, sizeN: 0.12, disposition: 'friendly' },
        ],
      },
    },
    sceneGraphNodes: [node('side', 's1', { isSideStoryStart: true, sideStoryLineTitle: 'Side', x: 0 })],
    assets: {
      [avatar]: {
        id: avatar,
        type: 'image',
        mime: 'image/png',
        originalName: 'a.png',
        relPath: 'assets/a.png',
        sha256: 'npcsha',
        sizeBytes: 1,
        createdAt: '2020-01-01T00:00:00.000Z',
      },
    },
    npcs: [
      {
        id: sourceNpcId,
        name: 'Hero',
        avatarAssetId: avatar,
        description: '',
        x: 0,
        y: 0,
        groupId: null,
        ringColor: '#aabbcc',
        disposition: 'neutral',
        imageOffset: { x: 0.1, y: -0.1 },
      imageScale: 1,
    },
    ],
  });
  const target = minimalProject({
    scenes: { [asSceneId('t1')]: scene('t1', 'Existing') },
    sceneGraphNodes: [node('tgn', 't1', { x: 0 })],
  });

  const { project: merged } = mergeStorylinesIntoProject(
    target,
    source,
    [{ kind: 'side', startGraphNodeId: asGraphNodeId('side') }],
    [{ sourceSceneId: asSceneId('s1'), mode: 'create' }],
    {
      graphOffsetX: 200,
      npcResolutions: [{ sourceNpcId, mode: 'create' }],
    },
  );

  assert.equal(merged.npcs.length, 1);
  const importedScene = Object.values(merged.scenes).find((s) => s.title === 'Side');
  assert.ok(importedScene);
  assert.equal(importedScene!.npcTokens.length, 1);
  assert.equal(importedScene!.npcTokens[0]!.npcId, merged.npcs[0]!.id);
  assert.notEqual(importedScene!.npcTokens[0]!.npcId, sourceNpcId);
  assert.equal(merged.npcs[0]!.ringColor, '#aabbcc');
});

void test('mergeStorylinesIntoProject: imports all NPCs from export bundle', () => {
  const avatar = asAssetId('a1');
  const sourceNpcId = asNpcId('src_npc');
  const source = minimalProject({
    scenes: { [asSceneId('s1')]: scene('s1', 'Side') },
    sceneGraphNodes: [node('side', 's1', { isSideStoryStart: true, sideStoryLineTitle: 'Side', x: 0 })],
    assets: {
      [avatar]: {
        id: avatar,
        type: 'image',
        mime: 'image/png',
        originalName: 'a.png',
        relPath: 'assets/a.png',
        sha256: 'npcsha',
        sizeBytes: 1,
        createdAt: '2020-01-01T00:00:00.000Z',
      },
    },
    npcs: [
      {
        id: sourceNpcId,
        name: 'Hero',
        avatarAssetId: avatar,
        description: '',
        x: 0,
        y: 0,
        groupId: null,
        ringColor: '#c9a227',
        disposition: 'neutral',
        imageOffset: { x: 0, y: 0 },
      imageScale: 1,
    },
    ],
  });
  const target = minimalProject({
    scenes: { [asSceneId('t1')]: scene('t1', 'Existing') },
    sceneGraphNodes: [node('tgn', 't1', { x: 0 })],
  });

  assert.equal(listExportedNpcsFromBundle(source).length, 1);

  const { project: merged, report } = mergeStorylinesIntoProject(
    target,
    source,
    [{ kind: 'side', startGraphNodeId: asGraphNodeId('side') }],
    [{ sourceSceneId: asSceneId('s1'), mode: 'create' }],
    {
      graphOffsetX: 200,
      npcResolutions: [{ sourceNpcId, mode: 'create' }],
    },
  );

  assert.equal(report.npcsCreated, 1);
  assert.equal(merged.npcs.length, 1);
  assert.equal(merged.npcs[0]!.name, 'Hero');
  assert.ok(!('binding' in (merged.npcs[0] as object)));
});

void test('mergeStorylinesIntoProject: reuse existing scene by resolution', () => {
  const targetSceneId = asSceneId('t1');
  const target = minimalProject({
    scenes: { [targetSceneId]: scene('t1', 'Tavern') },
    sceneGraphNodes: [],
  });

  const source = minimalProject({
    scenes: { [asSceneId('s1')]: scene('s1', 'Tavern') },
    sceneGraphNodes: [node('gn', 's1', { isStartScene: true, x: 0 })],
    sceneGraphEdges: [],
  });

  const { project: merged, report } = mergeStorylinesIntoProject(
    target,
    source,
    [{ kind: 'main' }],
    [{ sourceSceneId: asSceneId('s1'), mode: 'use', targetSceneId }],
    { graphOffsetX: 100 },
  );

  assert.equal(report.scenesReused, 1);
  assert.equal(report.scenesCreated, 0);
  assert.equal(merged.sceneGraphNodes.length, 1);
  assert.equal(merged.sceneGraphNodes[0]!.sceneId, targetSceneId);
});
