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

vi.mock('../../../../packages/agent-graph/src/canvas/render-cache', async () => {
  const actual = await vi.importActual<
    typeof import('../../../../packages/agent-graph/src/canvas/render-cache')
  >('../../../../packages/agent-graph/src/canvas/render-cache');

  return {
    ...actual,
    getAgentGlowSprite: vi.fn(() => ({ width: 1, height: 1 })),
  };
});

import { drawAgents } from '../../../../packages/agent-graph/src/canvas/draw-agents';

import type { GraphNode } from '@claude-teams/agent-graph';

interface FillTextCall {
  text: string;
  x: number;
  y: number;
  fillStyle: string;
  globalAlpha: number;
}

interface GradientStopCall {
  offset: number;
  color: string;
}

function createMockContext() {
  const fillTextCalls: FillTextCall[] = [];
  const strokeTextCalls: FillTextCall[] = [];
  const roundRectCalls: { x: number; y: number; width: number; height: number }[] = [];
  const gradientStops: GradientStopCall[] = [];
  const gradient = {
    addColorStop: vi.fn((offset: number, color: string) => {
      gradientStops.push({ offset, color });
    }),
  };
  let fillStyle = '';
  let globalAlpha = 1;

  const ctx = {
    save: vi.fn(),
    restore: vi.fn(),
    beginPath: vi.fn(),
    closePath: vi.fn(),
    moveTo: vi.fn(),
    lineTo: vi.fn(),
    arc: vi.fn(),
    fill: vi.fn(),
    stroke: vi.fn(),
    clip: vi.fn(),
    drawImage: vi.fn(),
    setLineDash: vi.fn(),
    clearRect: vi.fn(),
    fillRect: vi.fn(),
    strokeRect: vi.fn(),
    translate: vi.fn(),
    scale: vi.fn(),
    roundRect: vi.fn((x: number, y: number, width: number, height: number) => {
      roundRectCalls.push({ x, y, width, height });
    }),
    createRadialGradient: vi.fn(() => gradient),
    createLinearGradient: vi.fn(() => gradient),
    measureText: vi.fn((text: string) => ({ width: text.length * 4.5 })),
    fillText: vi.fn((text: string, x: number, y: number) => {
      fillTextCalls.push({ text, x, y, fillStyle, globalAlpha });
    }),
    strokeText: vi.fn((text: string, x: number, y: number) => {
      strokeTextCalls.push({ text, x, y, fillStyle, globalAlpha });
    }),
    shadowColor: '',
    shadowBlur: 0,
    shadowOffsetX: 0,
    shadowOffsetY: 0,
    get fillStyle() {
      return fillStyle;
    },
    set fillStyle(value: string) {
      fillStyle = value;
    },
    strokeStyle: '',
    lineWidth: 1,
    font: '',
    textAlign: 'left' as CanvasTextAlign,
    textBaseline: 'alphabetic' as CanvasTextBaseline,
    get globalAlpha() {
      return globalAlpha;
    },
    set globalAlpha(value: number) {
      globalAlpha = value;
    },
  } as unknown as CanvasRenderingContext2D;

  return { ctx, fillTextCalls, strokeTextCalls, roundRectCalls, gradientStops };
}

describe('drawAgents', () => {
  it('renders the active tool card above the node while keeping labels below it', () => {
    const { ctx, fillTextCalls, roundRectCalls } = createMockContext();
    const node: GraphNode = {
      id: 'member:demo:alice',
      kind: 'member',
      label: '2beacon-desk-22345',
      state: 'tool_calling',
      color: '#f5b74d',
      runtimeLabel: 'Anthropic · Haiku 4.5 | Medium',
      domainRef: { kind: 'member', teamName: 'demo', memberName: 'alice' },
      activeTool: {
        name: 'Bash',
        preview: 'list_my_sessions',
        state: 'running',
        startedAt: '2026-04-15T10:00:00.000Z',
        source: 'runtime',
      },
      x: 320,
      y: 240,
    };

    drawAgents(ctx, [node], 0, null, null, null, 1);

    const toolCard = roundRectCalls.find((call) => call.height === 18);
    expect(toolCard).toBeDefined();
    expect(toolCard!.y + toolCard!.height).toBeLessThan(node.y! - 1);

    const labelCall = fillTextCalls.find((call) => call.text.includes('2beacon-desk-22345'));
    const runtimeCall = fillTextCalls.find((call) => call.text.includes('Anthropic'));
    const toolCall = fillTextCalls.find((call) => call.text.includes('Bash: list_my_sessions'));

    expect(labelCall).toBeDefined();
    expect(runtimeCall).toBeDefined();
    expect(toolCall).toBeDefined();
    expect(labelCall!.y).toBeGreaterThan(node.y!);
    expect(runtimeCall!.y).toBeGreaterThan(labelCall!.y);
    expect(toolCall!.y).toBeLessThan(node.y!);
  });

  it('renders launch text as a third label line and removes old ad-hoc waiting text', () => {
    const { ctx, fillTextCalls } = createMockContext();
    const node: GraphNode = {
      id: 'member:demo:alice',
      kind: 'member',
      label: 'alice',
      state: 'idle',
      color: '#60a5fa',
      runtimeLabel: 'Codex · GPT-5.4 Mini · Medium',
      launchVisualState: 'runtime_pending',
      launchStatusLabel: 'connecting',
      spawnStatus: 'online',
      domainRef: { kind: 'member', teamName: 'demo', memberName: 'alice' },
      x: 320,
      y: 240,
    };

    drawAgents(ctx, [node], 0, null, null, null, 1);

    const labelCall = fillTextCalls.find((call) => call.text === 'alice');
    const runtimeCall = fillTextCalls.find((call) => call.text.includes('Codex'));
    const launchCall = fillTextCalls.find((call) => call.text === 'connecting');

    expect(labelCall).toBeDefined();
    expect(runtimeCall).toBeDefined();
    expect(launchCall).toBeDefined();
    expect(runtimeCall!.y).toBeGreaterThan(labelCall!.y);
    expect(launchCall!.y).toBeGreaterThan(runtimeCall!.y);
    expect(fillTextCalls.some((call) => call.text === 'waiting...')).toBe(false);
    expect(fillTextCalls.some((call) => call.text === 'connecting...')).toBe(false);
  });

  it('draws member labels with fixed high-contrast text and backdrops', () => {
    const { ctx, fillTextCalls, strokeTextCalls, roundRectCalls } = createMockContext();
    const node: GraphNode = {
      id: 'member:demo:alice',
      kind: 'member',
      label: 'alice',
      role: 'reviewer',
      state: 'idle',
      color: '#0000ff',
      runtimeLabel: 'Anthropic · Opus 4.6',
      domainRef: { kind: 'member', teamName: 'demo', memberName: 'alice' },
      x: 320,
      y: 240,
    };

    drawAgents(ctx, [node], 0, null, null, null, 1);

    const labelCall = fillTextCalls.find((call) => call.text === 'alice · reviewer');
    const runtimeCall = fillTextCalls.find((call) => call.text.includes('Anthropic'));

    expect(labelCall).toBeDefined();
    expect(runtimeCall).toBeDefined();
    expect(labelCall?.fillStyle).toBe('#e8f8ff');
    expect(runtimeCall?.fillStyle).toBe('#b9d7f2');
    expect(roundRectCalls.filter((call) => call.height === 12 || call.height === 10)).toHaveLength(
      2
    );
    expect(strokeTextCalls.some((call) => call.text === 'alice · reviewer')).toBe(true);
    expect(strokeTextCalls.some((call) => call.text.includes('Anthropic'))).toBe(true);
  });

  it('keeps lead labels readable when the lead node is visually dimmed', () => {
    const { ctx, fillTextCalls, roundRectCalls } = createMockContext();
    const node: GraphNode = {
      id: 'lead:demo',
      kind: 'lead',
      label: 'signal-ops-12',
      state: 'terminated',
      color: '#0000ff',
      runtimeLabel: 'GPT-5.4',
      domainRef: { kind: 'lead', teamName: 'demo', memberName: 'signal-ops-12' },
      x: 320,
      y: 240,
    };

    drawAgents(ctx, [node], 0, null, null, null, 1);

    const labelCall = fillTextCalls.find((call) => call.text === 'signal-ops-12');
    const runtimeCall = fillTextCalls.find((call) => call.text === 'GPT-5.4');

    expect(labelCall).toMatchObject({ fillStyle: '#e8f8ff', globalAlpha: 0.88 });
    expect(runtimeCall).toMatchObject({ fillStyle: '#b9d7f2', globalAlpha: 0.88 });
    expect(roundRectCalls.filter((call) => call.height === 12 || call.height === 10)).toHaveLength(
      2
    );
  });

  it('adds a red glow around members with error exceptions', () => {
    const { ctx, gradientStops } = createMockContext();
    const node: GraphNode = {
      id: 'member:demo:bob',
      kind: 'member',
      label: 'bob',
      state: 'active',
      color: '#7c3aed',
      exceptionTone: 'error',
      exceptionLabel: 'OpenCode API error',
      domainRef: { kind: 'member', teamName: 'demo', memberName: 'bob' },
      x: 320,
      y: 240,
    };

    drawAgents(ctx, [node], 0, null, null, null, 1);

    expect(ctx.createRadialGradient).toHaveBeenCalledWith(320, 240, 18, 320, 240, 50);
    expect(gradientStops.some((stop) => stop.color.startsWith('#ef4444'))).toBe(true);
  });
});
