import { describe, expect, it } from 'vitest';

import {
  buildStableSlotLayoutSnapshot,
  resolveNearestSlotAssignment,
  type StableRect,
  type StableSlotLayoutSnapshot,
  validateStableSlotLayout,
} from '../../packages/agent-graph/src/layout/stableSlots';

import type {
  GraphLayoutPort,
  GraphNode,
  GraphOwnerSlotAssignment,
} from '../../packages/agent-graph/src/ports/types';

function ownerNode(id: string, kind: 'lead' | 'member' = 'member'): GraphNode {
  return {
    id,
    kind,
    label: id,
    state: 'idle',
    domainRef: {
      kind,
      teamName: 'test-team',
      memberName: id,
    },
  };
}

function taskNode(id: string, ownerId: string, index: number): GraphNode {
  return {
    id,
    kind: 'task',
    label: id,
    state: 'idle',
    ownerId,
    taskStatus: index === 0 ? 'in_progress' : 'pending',
    reviewState: index === 1 ? 'review' : 'none',
    domainRef: {
      kind: 'task',
      teamName: 'test-team',
      taskId: id,
    },
  };
}

function buildOwnerGraph(
  ownerCount: number,
  slotAssignments: Record<string, GraphOwnerSlotAssignment>
): { nodes: GraphNode[]; layout: GraphLayoutPort } {
  const nodes: GraphNode[] = [ownerNode('lead', 'lead')];
  const ownerOrder = Array.from({ length: ownerCount }, (_, index) => `member-${index}`);

  ownerOrder.forEach((ownerId, ownerIndex) => {
    nodes.push(ownerNode(ownerId));

    for (let taskIndex = 0; taskIndex < 3; taskIndex += 1) {
      nodes.push(taskNode(`task-${ownerIndex}-${taskIndex}`, ownerId, taskIndex));
    }
  });

  return {
    nodes,
    layout: {
      version: 'stable-slots-v1',
      mode: 'radial',
      ownerOrder,
      slotAssignments,
    },
  };
}

function buildSixOwnerGraph(): { nodes: GraphNode[]; layout: GraphLayoutPort } {
  return buildOwnerGraph(
    6,
    Object.fromEntries(
      Array.from({ length: 6 }, (_, index) => [
        `member-${index}`,
        { ringIndex: 0, sectorIndex: index },
      ])
    )
  );
}

function buildRowOrbitGraph(
  ownerCount: number,
  rowCounts: readonly number[]
): {
  nodes: GraphNode[];
  layout: GraphLayoutPort;
} {
  const assignments: Record<string, GraphOwnerSlotAssignment> = {};
  let ownerIndex = 0;
  rowCounts.forEach((columnCount, ringIndex) => {
    for (let sectorIndex = 0; sectorIndex < columnCount; sectorIndex += 1) {
      assignments[`member-${ownerIndex}`] = { ringIndex, sectorIndex };
      ownerIndex += 1;
    }
  });
  return buildOwnerGraph(ownerCount, assignments);
}

function getSnapshot(nodes: GraphNode[], layout: GraphLayoutPort): StableSlotLayoutSnapshot {
  const snapshot = buildStableSlotLayoutSnapshot({
    teamName: 'test-team',
    nodes,
    layout,
  });
  expect(snapshot).not.toBeNull();
  expect(validateStableSlotLayout(snapshot!)).toEqual({ valid: true });
  return snapshot!;
}

function rectsOverlap(left: StableRect, right: StableRect): boolean {
  return (
    left.left < right.right &&
    left.right > right.left &&
    left.top < right.bottom &&
    left.bottom > right.top
  );
}

function getRowCounts(snapshot: StableSlotLayoutSnapshot): number[] {
  const rowCounts = new Map<number, number>();
  for (const frame of snapshot.memberSlotFrames) {
    rowCounts.set(frame.ringIndex, (rowCounts.get(frame.ringIndex) ?? 0) + 1);
  }
  return Array.from(rowCounts.entries())
    .sort(([left], [right]) => left - right)
    .map(([, count]) => count);
}

function getRowWidths(snapshot: StableSlotLayoutSnapshot): number[] {
  const rows = new Map<number, StableRect[]>();
  for (const frame of snapshot.memberSlotFrames) {
    rows.set(frame.ringIndex, [...(rows.get(frame.ringIndex) ?? []), frame.bounds]);
  }
  return Array.from(rows.entries())
    .sort(([left], [right]) => left - right)
    .map(([, rects]) => {
      const left = Math.min(...rects.map((rect) => rect.left));
      const right = Math.max(...rects.map((rect) => rect.right));
      return right - left;
    });
}

describe('stable slot layout', () => {
  it('packs six legacy radial owners into two row-orbit rows', () => {
    const { nodes, layout } = buildSixOwnerGraph();
    const snapshot = getSnapshot(nodes, layout);

    expect(snapshot.ownerSlotLayoutKind).toBe('row-orbit');
    expect(getRowCounts(snapshot)).toEqual([3, 3]);
    expect(snapshot.memberSlotFrames.map((frame) => frame.ringIndex)).toEqual([0, 0, 0, 2, 2, 2]);
    expect(snapshot.memberSlotFrames.map((frame) => frame.sectorIndex)).toEqual([0, 1, 2, 0, 1, 2]);
  });

  it('lets six radial owners move into an empty lead-level side slot', () => {
    const { nodes, layout } = buildSixOwnerGraph();
    const snapshot = getSnapshot(nodes, layout);
    const currentFrame = snapshot.memberSlotFrameByOwnerId.get('member-0')!;
    const targetOwnerX =
      snapshot.runtimeCentralExclusion.left - 160 - currentFrame.bounds.width / 2;

    const result = resolveNearestSlotAssignment({
      ownerId: 'member-0',
      ownerX: targetOwnerX,
      ownerY: 0,
      nodes,
      snapshot,
      layout,
    });

    expect(result).toMatchObject({
      assignment: { ringIndex: 1, sectorIndex: 0 },
      previewOwnerX: targetOwnerX,
      previewOwnerY: 0,
    });
    expect(result?.displacedOwnerId).toBeUndefined();

    const nextSnapshot = getSnapshot(nodes, {
      ...layout,
      slotAssignments: {
        ...layout.slotAssignments,
        'member-0': result!.assignment,
      },
    });
    expect(nextSnapshot.ownerSlotLayoutKind).toBe('row-orbit');
    expect(nextSnapshot.memberSlotFrameByOwnerId.get('member-0')).toMatchObject({
      ringIndex: 1,
      sectorIndex: 0,
    });
  });

  it('uses two grid columns for six owners in rows layout', () => {
    const { nodes, layout } = buildSixOwnerGraph();
    const snapshot = getSnapshot(nodes, {
      ...layout,
      mode: 'grid-under-lead',
      slotAssignments: {},
    });

    expect(snapshot.ownerSlotLayoutKind).toBe('grid-under-lead');
    expect(snapshot.memberSlotFrames.map((frame) => frame.ringIndex)).toEqual([0, 0, 1, 1, 2, 2]);
    expect(snapshot.memberSlotFrames.map((frame) => frame.sectorIndex)).toEqual([0, 1, 0, 1, 0, 1]);
  });

  it('packs eight radial owners into row-orbit rows without crossing the lead exclusion', () => {
    const { nodes, layout } = buildRowOrbitGraph(8, [3, 2, 3]);
    const snapshot = getSnapshot(nodes, layout);

    expect(snapshot.ownerSlotLayoutKind).toBe('row-orbit');
    expect(getRowCounts(snapshot)).toEqual([3, 2, 3]);

    const leadRowFrames = snapshot.memberSlotFrames.filter((frame) => frame.ringIndex === 1);
    expect(leadRowFrames).toHaveLength(2);
    for (const frame of leadRowFrames) {
      expect(rectsOverlap(frame.bounds, snapshot.runtimeCentralExclusion)).toBe(false);
      if (frame.ownerX < 0) {
        expect(frame.bounds.right).toBeLessThanOrEqual(snapshot.runtimeCentralExclusion.left - 160);
      } else {
        expect(frame.bounds.left).toBeGreaterThanOrEqual(
          snapshot.runtimeCentralExclusion.right + 160
        );
      }
    }
  });

  it('packs twelve radial owners into four safe rows with no four-column row width', () => {
    const { nodes, layout } = buildRowOrbitGraph(12, [3, 3, 3, 3]);
    const snapshot = getSnapshot(nodes, layout);

    expect(snapshot.ownerSlotLayoutKind).toBe('row-orbit');
    expect(getRowCounts(snapshot)).toEqual([3, 3, 3, 3]);

    const maxFrameWidth = Math.max(...snapshot.memberSlotFrames.map((frame) => frame.bounds.width));
    const maxRowWidth = Math.max(...getRowWidths(snapshot));
    expect(maxRowWidth).toBeLessThan(maxFrameWidth * 4);
  });

  it('swaps with the nearest existing row-orbit slot while dragging', () => {
    const { nodes, layout } = buildRowOrbitGraph(8, [3, 2, 3]);
    const snapshot = getSnapshot(nodes, layout);
    const currentFrame = snapshot.memberSlotFrameByOwnerId.get('member-0')!;
    const targetFrame = snapshot.memberSlotFrameByOwnerId.get('member-4')!;

    const result = resolveNearestSlotAssignment({
      ownerId: 'member-0',
      ownerX: targetFrame.ownerX,
      ownerY: targetFrame.ownerY,
      nodes,
      snapshot,
      layout,
    });

    expect(result).toEqual({
      assignment: {
        ringIndex: targetFrame.ringIndex,
        sectorIndex: targetFrame.sectorIndex,
      },
      displacedOwnerId: 'member-4',
      displacedAssignment: {
        ringIndex: currentFrame.ringIndex,
        sectorIndex: currentFrame.sectorIndex,
      },
      previewOwnerX: targetFrame.ownerX,
      previewOwnerY: targetFrame.ownerY,
    });
  });
});
