import { describe, expect, it } from 'vitest';
import {
  buildFileHistoryIndex,
  buildVersionGraph,
  layoutVersionGraph,
  versionFraction,
} from './versionGraph';

const F = 'Assets/a.bin';
const HA = 'a'.repeat(64);
const HB = 'b'.repeat(64);
const HC = 'c'.repeat(64);

// Newest-first, like the server. changes: [path, change, hash?].
function rev(revision, changes, description = `rev ${revision}`) {
  return {
    revision,
    author: '@alex',
    date: '2026-07-11 10:00',
    description,
    changes: changes.map(([path, change, hash]) =>
      change === 'deleted' ? { path, change } : { path, change, hash, size: 10 }
    ),
  };
}

function historyFor(revisions, path = F) {
  return buildFileHistoryIndex(revisions).get(path) ?? [];
}

describe('buildFileHistoryIndex', () => {
  it('groups by path, ascending revision, null hash for deletes', () => {
    const index = buildFileHistoryIndex([
      rev(3, [[F, 'deleted']]),
      rev(1, [[F, 'added', HA]]),
      rev(2, [[F, 'edited', HB], ['other.bin', 'added', HC]]),
    ]);
    expect(index.get(F).map((h) => [h.revision, h.change, h.hash])).toEqual([
      [1, 'added', HA],
      [2, 'edited', HB],
      [3, 'deleted', null],
    ]);
    expect(index.get('other.bin')).toHaveLength(1);
  });
});

describe('buildVersionGraph', () => {
  it('numbers unique content states and links parents linearly', () => {
    const history = historyFor([
      rev(1, [[F, 'added', HA]]),
      rev(2, [[F, 'edited', HB]]),
      rev(3, [[F, 'deleted']]),
    ]);
    const { nodes } = buildVersionGraph(history, { serverHeadHash: null });
    expect(nodes.map((n) => [n.version, n.change, n.parent])).toEqual([
      [1, 'added', null],
      [2, 'edited', 1],
      [3, 'deleted', 2],
    ]);
    expect(nodes[0].children).toEqual([2]);
    expect(nodes[1].children).toEqual([3]);
    expect(nodes[0].firstRevision).toBe(1);
  });

  it('revisits an existing state on revert and on delete-again (no new node)', () => {
    // add, edit, delete, back-to-edit content, delete again.
    const history = historyFor([
      rev(1, [[F, 'added', HA]]),
      rev(2, [[F, 'edited', HB]]),
      rev(3, [[F, 'deleted']]),
      rev(4, [[F, 'edited', HB]]), // content == v2 -> revisit v2
      rev(5, [[F, 'deleted']]),    // content == v3 -> revisit v3
    ]);
    const { nodes, currentVersion } = buildVersionGraph(history, { serverHeadHash: null });
    expect(nodes.map((n) => n.version)).toEqual([1, 2, 3]); // still only 3 states
    expect(currentVersion).toBe(3); // server head deleted -> current is the DELETED node
  });

  it('dedups identical content produced by a plain edit', () => {
    const history = historyFor([
      rev(1, [[F, 'added', HA]]),
      rev(2, [[F, 'edited', HB]]),
      rev(3, [[F, 'edited', HA]]), // content back to HA -> revisit v1
    ]);
    const { nodes, currentVersion, localVersion } = buildVersionGraph(history, {
      serverHeadHash: HA,
      syncedHash: HA,
    });
    expect(nodes).toHaveLength(2);
    expect(currentVersion).toBe(1); // interior current
    expect(localVersion).toBe(1);
    expect(nodes.find((n) => n.version === 2).isDead).toBe(true); // v2 off the root->current path
  });

  it('branches when new content is produced from an interior current', () => {
    const history = historyFor([
      rev(1, [[F, 'added', HA]]),
      rev(2, [[F, 'edited', HB]]),
      rev(3, [[F, 'deleted']]),
      rev(4, [[F, 'edited', HB]]), // revisit v2
      rev(5, [[F, 'edited', HC]]), // new content from v2 -> new node v4, branch
    ]);
    const { nodes } = buildVersionGraph(history, { serverHeadHash: HC });
    const byV = Object.fromEntries(nodes.map((n) => [n.version, n]));
    expect(byV[2].children).toEqual([3, 4]); // ascending: older (v3) first
    expect(byV[4].parent).toBe(2);
    expect(nodes.filter((n) => n.isDead).map((n) => n.version)).toEqual([3]);
  });

  it('marks current by server head hash and dims everything off its path', () => {
    const history = historyFor([
      rev(1, [[F, 'added', HA]]),
      rev(2, [[F, 'edited', HB]]),
      rev(3, [[F, 'deleted']]),
    ]);
    const { nodes, currentVersion } = buildVersionGraph(history, { serverHeadHash: HB });
    expect(currentVersion).toBe(2);
    expect(nodes.find((n) => n.version === 2).isCurrent).toBe(true);
    expect(nodes.filter((n) => n.isDead).map((n) => n.version)).toEqual([3]);
  });

  it('treats a revert-to-before-existence as a plain DELETED node', () => {
    // A rollback to before the file existed arrives as change "deleted".
    const history = historyFor([
      rev(1, [[F, 'added', HA]]),
      rev(2, [[F, 'deleted']]),
    ]);
    const { nodes } = buildVersionGraph(history, { serverHeadHash: null });
    expect(nodes[1].change).toBe('deleted'); // never "reverted"
  });

  it('sets the local node from the synced hash', () => {
    const history = historyFor([
      rev(1, [[F, 'added', HA]]),
      rev(2, [[F, 'edited', HB]]),
      rev(3, [[F, 'edited', HC]]),
    ]);
    const { nodes, localVersion } = buildVersionGraph(history, {
      serverHeadHash: HC,
      syncedHash: HB,
    });
    expect(localVersion).toBe(2);
    expect(nodes.find((n) => n.version === 2).isLocal).toBe(true);
  });

  it('assigns depth as distance from the root, so sibling branches share it', () => {
    // add A, edit B, revert to A, edit C (2nd child of A), revert to B, delete
    // (child of B). Generations: A=1, B=2, C=2, D=3 (displayed as depth+1).
    const history = historyFor([
      rev(1, [[F, 'added', HA]]),
      rev(2, [[F, 'edited', HB]]),
      rev(3, [[F, 'edited', HA]]), // revisit A
      rev(4, [[F, 'edited', HC]]), // new content from A -> 2nd child of A
      rev(5, [[F, 'edited', HB]]), // revisit B
      rev(6, [[F, 'deleted']]), // new content from B -> child of B
    ]);
    const { nodes } = buildVersionGraph(history, { serverHeadHash: null });
    // nodes are in creation order: A, B, C, D
    expect(nodes.map((n) => n.depth)).toEqual([0, 1, 1, 2]);
    expect(nodes.map((n) => n.depth + 1)).toEqual([1, 2, 2, 3]);
  });
});

describe('versionFraction', () => {
  const history = () =>
    historyFor([
      rev(1, [[F, 'added', HA]]),
      rev(2, [[F, 'edited', HB]]),
      rev(3, [[F, 'edited', HC]]),
    ]);

  it('reads your/server when both hashes match a node', () => {
    expect(versionFraction(history(), HC, HB)).toEqual({ your: 2, server: 3 });
  });
  it('reads n/n when synced equals head', () => {
    expect(versionFraction(history(), HC, HC)).toEqual({ your: 3, server: 3 });
  });
  it('reflects a server revert as your > server', () => {
    expect(versionFraction(history(), HA, HC)).toEqual({ your: 3, server: 1 });
  });
  it('is null when the synced hash matches no node', () => {
    expect(versionFraction(history(), HC, 'z'.repeat(64))).toBeNull();
  });
  it('is null when the server head matches no node', () => {
    expect(versionFraction(history(), 'z'.repeat(64), HC)).toBeNull();
  });
  it('is null with no history', () => {
    expect(versionFraction([], HC, HC)).toBeNull();
  });
  it('counts generations from the root, not creation order', () => {
    // add A, edit B, revert to A, edit C: B and C are both generation 2 though
    // created 2nd and 3rd. Server on C, local on B -> 2/2 (creation order would
    // wrongly read 2/3).
    const branched = historyFor([
      rev(1, [[F, 'added', HA]]),
      rev(2, [[F, 'edited', HB]]),
      rev(3, [[F, 'edited', HA]]),
      rev(4, [[F, 'edited', HC]]),
    ]);
    expect(versionFraction(branched, HC, HB)).toEqual({ your: 2, server: 2 });
  });
});

describe('layoutVersionGraph', () => {
  it('lays a linear chain in one column', () => {
    const { nodes } = buildVersionGraph(
      historyFor([rev(1, [[F, 'added', HA]]), rev(2, [[F, 'edited', HB]])]),
      { serverHeadHash: HB }
    );
    layoutVersionGraph(nodes);
    expect(nodes.map((n) => [n.x, n.y])).toEqual([[0, 0], [0, 1]]);
  });

  it('puts the older branch left and centers ancestors', () => {
    const { nodes } = buildVersionGraph(
      historyFor([
        rev(1, [[F, 'added', HA]]),
        rev(2, [[F, 'edited', HB]]),
        rev(3, [[F, 'deleted']]),
        rev(4, [[F, 'edited', HB]]),
        rev(5, [[F, 'edited', HC]]),
      ]),
      { serverHeadHash: HC }
    );
    layoutVersionGraph(nodes);
    const byV = Object.fromEntries(nodes.map((n) => [n.version, n]));
    expect(byV[3].x).toBeLessThan(byV[4].x);
    expect(byV[2].x).toBe((byV[3].x + byV[4].x) / 2);
    expect(byV[1].x).toBe(byV[2].x);
    expect([byV[1].y, byV[2].y, byV[3].y, byV[4].y]).toEqual([0, 1, 2, 2]);
  });
});
