import React, { act } from 'react';
import { createRoot } from 'react-dom/client';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { GraphMemberLogPreviewHud } from '@features/agent-graph/renderer/ui/GraphMemberLogPreviewHud';

import type { GraphNode } from '@claude-teams/agent-graph';
import type { MemberLogPreviewMember } from '@features/member-log-stream/contracts/dto';

const basePreviewsByMember = new Map<string, MemberLogPreviewMember>([
  [
    'team-lead',
    {
      memberName: 'team-lead',
      items: [
        {
          id: 'lead-preview-1',
          kind: 'text' as const,
          provider: 'claude_transcript' as const,
          timestamp: '2026-04-03T00:00:00.000Z',
          title: 'Assistant',
          preview: 'lead log preview',
          tone: 'neutral' as const,
        },
      ],
      coverage: [{ provider: 'claude_transcript' as const, status: 'included' as const }],
      warnings: [],
      truncated: false,
      overflowCount: 0,
      generatedAt: '2026-04-03T00:00:00.000Z',
    },
  ],
  [
    'alice',
    {
      memberName: 'alice',
      items: [
        {
          id: 'preview-1',
          kind: 'tool_use' as const,
          provider: 'claude_transcript' as const,
          timestamp: '2026-04-03T00:00:00.000Z',
          title: 'Bash',
          preview: 'pnpm test',
          tone: 'warning' as const,
        },
        {
          id: 'preview-2',
          kind: 'tool_result' as const,
          provider: 'opencode_runtime' as const,
          timestamp: '2026-04-03T00:00:30.000Z',
          title: 'Send message error',
          preview: 'OpenCode tool failed without output',
          tone: 'error' as const,
        },
        {
          id: 'preview-3',
          kind: 'tool_result' as const,
          provider: 'opencode_runtime' as const,
          timestamp: '2026-04-03T00:00:40.000Z',
          title: 'Bash result',
          preview: 'Tests passed',
          tone: 'success' as const,
        },
      ],
      coverage: [{ provider: 'claude_transcript' as const, status: 'included' as const }],
      warnings: [],
      truncated: true,
      overflowCount: 2,
      generatedAt: '2026-04-03T00:00:00.000Z',
    },
  ],
]);
let mockedPreviewsByMember = basePreviewsByMember;

vi.mock('@features/agent-graph/renderer/hooks/useGraphMemberLogPreviews', () => ({
  buildGraphLogPreviewLaneIdsByMember: () => ({ alice: 'secondary:opencode:alice' }),
  useGraphMemberLogPreviews: () => ({
    previewsByMember: mockedPreviewsByMember,
    loading: false,
    error: null,
    reload: vi.fn(),
  }),
}));

vi.mock('@features/agent-graph/renderer/hooks/useGraphActivityContext', () => ({
  useGraphActivityContext: () => ({
    teamData: {
      members: [
        {
          name: 'alice',
          status: 'active',
          currentTaskId: null,
          taskCount: 0,
          lastActiveAt: null,
          messageCount: 0,
          providerId: 'opencode',
          laneOwnerProviderId: 'opencode',
          laneId: 'secondary:opencode:alice',
        },
      ],
    },
  }),
}));

describe('GraphMemberLogPreviewHud', () => {
  beforeEach(() => {
    vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
    vi.stubGlobal(
      'requestAnimationFrame',
      vi.fn(() => 1)
    );
    vi.stubGlobal('cancelAnimationFrame', vi.fn());
    vi.useFakeTimers();
    vi.setSystemTime(new Date('2026-04-03T00:01:00.000Z'));
    mockedPreviewsByMember = basePreviewsByMember;
  });

  afterEach(() => {
    document.body.innerHTML = '';
    vi.useRealTimers();
    vi.unstubAllGlobals();
  });

  it('opens the member profile on the logs tab when a preview row or overflow is clicked', async () => {
    const node: GraphNode = {
      id: 'member:alpha-team:alice',
      kind: 'member',
      label: 'alice',
      state: 'active',
      domainRef: { kind: 'member', teamName: 'alpha-team', memberName: 'alice' },
    };
    const onOpenMemberProfile = vi.fn();
    const host = document.createElement('div');
    document.body.appendChild(host);
    const root = createRoot(host);

    await act(async () => {
      root.render(
        <GraphMemberLogPreviewHud
          teamName="alpha-team"
          nodes={[node]}
          getLogWorldRect={() => ({
            left: 40,
            top: 80,
            right: 300,
            bottom: 372,
            width: 260,
            height: 292,
          })}
          getCameraZoom={() => 1}
          worldToScreen={(x, y) => ({ x, y })}
          getViewportSize={() => ({ width: 1200, height: 800 })}
          focusNodeIds={null}
          onOpenMemberProfile={onOpenMemberProfile}
        />
      );
      await Promise.resolve();
    });

    const row = Array.from(host.querySelectorAll('button')).find((button) =>
      button.textContent?.includes('pnpm test')
    );
    expect(row).not.toBeUndefined();
    expect(row?.querySelector('.float-left')).not.toBeNull();
    expect(row?.querySelector('.line-clamp-3')).toBeNull();
    expect(row?.className).toContain('h-16');
    expect(row?.querySelector('span.text-slate-200')?.className).toContain('leading-4');
    expect(row?.textContent).toContain('pnpm test');

    const errorRow = Array.from(host.querySelectorAll('button')).find((button) =>
      button.textContent?.includes('OpenCode tool failed')
    );
    expect(errorRow?.querySelector('svg.text-rose-300')).not.toBeNull();

    const resultRow = Array.from(host.querySelectorAll('button')).find((button) =>
      button.textContent?.includes('Tests passed')
    );
    expect(resultRow?.textContent).toContain('Bash');
    expect(resultRow?.textContent).not.toContain('Bash result');

    await act(async () => {
      row?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
      await Promise.resolve();
    });

    expect(onOpenMemberProfile).toHaveBeenCalledWith('alice', { initialTab: 'logs' });

    const moreButton = Array.from(host.querySelectorAll('button')).find((button) =>
      button.textContent?.includes('+2 more')
    );
    expect(moreButton).not.toBeUndefined();

    await act(async () => {
      moreButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
      await Promise.resolve();
    });

    expect(onOpenMemberProfile).toHaveBeenCalledTimes(2);

    act(() => {
      root.unmount();
    });
  });

  it('briefly highlights a newly appeared preview row', async () => {
    const node: GraphNode = {
      id: 'member:alpha-team:alice',
      kind: 'member',
      label: 'alice',
      state: 'active',
      domainRef: { kind: 'member', teamName: 'alpha-team', memberName: 'alice' },
    };
    const host = document.createElement('div');
    document.body.appendChild(host);
    const root = createRoot(host);
    const renderHud = (): void => {
      root.render(
        <GraphMemberLogPreviewHud
          teamName="alpha-team"
          nodes={[node]}
          getLogWorldRect={() => ({
            left: 40,
            top: 80,
            right: 300,
            bottom: 372,
            width: 260,
            height: 292,
          })}
          getCameraZoom={() => 1}
          worldToScreen={(x, y) => ({ x, y })}
          getViewportSize={() => ({ width: 1200, height: 800 })}
          focusNodeIds={null}
        />
      );
    };

    await act(async () => {
      renderHud();
      await Promise.resolve();
    });

    const alicePreview = basePreviewsByMember.get('alice')!;
    mockedPreviewsByMember = new Map(basePreviewsByMember);
    mockedPreviewsByMember.set('alice', {
      ...alicePreview,
      items: [
        {
          id: 'preview-new',
          kind: 'text' as const,
          provider: 'claude_transcript' as const,
          timestamp: '2026-04-03T00:01:00.000Z',
          title: 'Assistant',
          preview: 'new compact log',
          tone: 'neutral' as const,
        },
        ...alicePreview.items,
      ],
    });

    await act(async () => {
      renderHud();
      await Promise.resolve();
    });

    const newRow = Array.from(host.querySelectorAll('button')).find((button) =>
      button.textContent?.includes('new compact log')
    );
    expect(newRow?.className).toContain('border-sky-300/70');

    await act(async () => {
      vi.advanceTimersByTime(1_000);
      await Promise.resolve();
    });

    expect(newRow?.className).not.toContain('border-sky-300/70');

    act(() => {
      root.unmount();
    });
  });

  it('renders lead log previews and opens the lead profile logs tab', async () => {
    const leadNode: GraphNode = {
      id: 'lead:alpha-team',
      kind: 'lead',
      label: 'alpha-team',
      state: 'active',
      domainRef: { kind: 'lead', teamName: 'alpha-team', memberName: 'team-lead' },
    };
    const onOpenMemberProfile = vi.fn();
    const host = document.createElement('div');
    document.body.appendChild(host);
    const root = createRoot(host);

    await act(async () => {
      root.render(
        <GraphMemberLogPreviewHud
          teamName="alpha-team"
          nodes={[leadNode]}
          getLogWorldRect={() => ({
            left: 40,
            top: 80,
            right: 300,
            bottom: 372,
            width: 260,
            height: 292,
          })}
          getCameraZoom={() => 1}
          worldToScreen={(x, y) => ({ x, y })}
          getViewportSize={() => ({ width: 1200, height: 800 })}
          focusNodeIds={null}
          onOpenMemberProfile={onOpenMemberProfile}
        />
      );
      await Promise.resolve();
    });

    const row = Array.from(host.querySelectorAll('button')).find((button) =>
      button.textContent?.includes('lead log preview')
    );
    expect(row).not.toBeUndefined();

    await act(async () => {
      row?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
      await Promise.resolve();
    });

    expect(onOpenMemberProfile).toHaveBeenCalledWith('team-lead', { initialTab: 'logs' });

    act(() => {
      root.unmount();
    });
  });

  it('keeps compact event text readable without repeating the title prefix', async () => {
    mockedPreviewsByMember = new Map<string, MemberLogPreviewMember>([
      [
        'alice',
        {
          memberName: 'alice',
          items: [
            {
              id: 'message-sent-preview',
              kind: 'tool_result',
              provider: 'claude_transcript',
              timestamp: '2026-04-03T00:01:00.000Z',
              title: 'Message sent',
              preview: 'Message sent to team-lead - #abc done',
              tone: 'success',
            },
            {
              id: 'generic-tool-result-preview',
              kind: 'tool_result',
              provider: 'claude_transcript',
              timestamp: '2026-04-03T00:00:50.000Z',
              title: 'Tool result',
              preview: 'stored',
              tone: 'success',
            },
          ],
          coverage: [{ provider: 'claude_transcript', status: 'included' }],
          warnings: [],
          truncated: false,
          overflowCount: 0,
          generatedAt: '2026-04-03T00:01:00.000Z',
        },
      ],
    ]);
    const node: GraphNode = {
      id: 'member:alpha-team:alice',
      kind: 'member',
      label: 'alice',
      state: 'active',
      domainRef: { kind: 'member', teamName: 'alpha-team', memberName: 'alice' },
    };
    const host = document.createElement('div');
    document.body.appendChild(host);
    const root = createRoot(host);

    await act(async () => {
      root.render(
        <GraphMemberLogPreviewHud
          teamName="alpha-team"
          nodes={[node]}
          getLogWorldRect={() => ({
            left: 40,
            top: 80,
            right: 300,
            bottom: 372,
            width: 260,
            height: 292,
          })}
          getCameraZoom={() => 1}
          worldToScreen={(x, y) => ({ x, y })}
          getViewportSize={() => ({ width: 1200, height: 800 })}
          focusNodeIds={null}
        />
      );
      await Promise.resolve();
    });

    const messageRow = Array.from(host.querySelectorAll('button')).find((button) =>
      button.textContent?.includes('#abc done')
    );
    expect(messageRow?.textContent).toContain('Message sent');
    expect(messageRow?.textContent).toContain('to team-lead - #abc done');
    expect(messageRow?.textContent).not.toContain('Message sentMessage sent');
    expect(messageRow?.textContent).not.toContain('Message sent now Message sent');

    const genericResultRow = Array.from(host.querySelectorAll('button')).find((button) =>
      button.textContent?.includes('stored')
    );
    expect(genericResultRow?.textContent).toContain('Tool result');

    act(() => {
      root.unmount();
    });
  });
});
