/**
 * Unit tests for `computeGraphRows` — the pure lane-layout algorithm.
 *
 * Each row is split into `incoming` (top boundary → node) and `outgoing`
 * (node → bottom boundary) links; a link with `fromLane !== toLane` is a bent
 * fork/merge connector, otherwise it is a straight pass-through lane.
 */

import { describe, it, expect } from "vitest";
import { computeGraphRows } from "./graphLayout";
import type { GraphCommit } from "@/domain";

function commit(hash: string, parents: string[]): GraphCommit {
  return { hash, summary: hash, parents, refs: [], author: "test", timestamp: 0 };
}

/** Just the (fromLane,toLane) pairs of a set of links. */
function pairs(links: { fromLane: number; toLane: number }[]) {
  return links.map((l) => ({ fromLane: l.fromLane, toLane: l.toLane }));
}

describe("computeGraphRows — linear history", () => {
  // c → b → a (root); newest first.
  const commits = [commit("c", ["b"]), commit("b", ["a"]), commit("a", [])];

  it("keeps all commits on lane 0", () => {
    const rows = computeGraphRows(commits);
    expect(rows.map((r) => r.lane)).toEqual([0, 0, 0]);
  });

  it("a non-root commit flows straight down lane 0", () => {
    const b = computeGraphRows(commits).find((r) => r.commit.hash === "b")!;
    expect(pairs(b.outgoing)).toEqual([{ fromLane: 0, toLane: 0 }]);
  });

  it("the root commit has no outgoing link (lane ends)", () => {
    const a = computeGraphRows(commits).find((r) => r.commit.hash === "a")!;
    expect(a.outgoing).toHaveLength(0);
    // It still receives the lane from the commit above it.
    expect(pairs(a.incoming)).toContainEqual({ fromLane: 0, toLane: 0 });
  });
});

describe("computeGraphRows — fork then converge", () => {
  // d→b, c→a, b→a, a root. Order newest-first: d, c, b, a.
  const commits = [
    commit("d", ["b"]),
    commit("c", ["a"]),
    commit("b", ["a"]),
    commit("a", []),
  ];
  const rows = computeGraphRows(commits);
  const by = Object.fromEntries(rows.map((r) => [r.commit.hash, r]));

  it("opens a second lane for the diverged tip", () => {
    expect(by["d"].lane).toBe(0);
    expect(by["c"].lane).toBe(1);
    expect(by["b"].lane).toBe(0);
  });

  it("converges the second lane into the shared ancestor", () => {
    // At commit `a`, lane 1 bends into lane 0 (the node).
    expect(pairs(by["a"].incoming)).toContainEqual({ fromLane: 1, toLane: 0 });
  });
});

describe("computeGraphRows — merge commit", () => {
  // e merges b and d. e→[b,d], d→[c], b→[a], c→[a], a root.
  const commits = [
    commit("e", ["b", "d"]),
    commit("d", ["c"]),
    commit("b", ["a"]),
    commit("c", ["a"]),
    commit("a", []),
  ];
  const rows = computeGraphRows(commits);
  const by = Object.fromEntries(rows.map((r) => [r.commit.hash, r]));

  it("the merge node sits on lane 0 and forks down to both parents", () => {
    expect(by["e"].lane).toBe(0);
    expect(pairs(by["e"].outgoing)).toEqual(
      expect.arrayContaining([
        { fromLane: 0, toLane: 0 }, // first parent keeps the lane
        { fromLane: 0, toLane: 1 }, // second parent diverges to a new lane
      ]),
    );
  });

  it("the second parent lands on the new lane", () => {
    expect(by["d"].lane).toBe(1);
  });
});

describe("computeGraphRows — colours", () => {
  it("gives distinct lanes distinct colours", () => {
    const rows = computeGraphRows([
      commit("x", ["z"]),
      commit("y", ["z"]),
      commit("z", []),
    ]);
    const by = Object.fromEntries(rows.map((r) => [r.commit.hash, r]));
    expect(by["x"].color).not.toBe(by["y"].color);
  });

  it("returns a hex colour per row", () => {
    const rows = computeGraphRows([commit("a", [])]);
    expect(rows[0].color).toMatch(/^#[0-9a-f]{6}$/i);
  });
});
