import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TurnCard } from './TurnCard';
import { Turn, EventRow, UiAgentInfo } from '../types';

// Mock heavy child components to isolate TurnCard logic
vi.mock('./MessageContent', () => ({
  MessageContent: ({ content }: { content: string }) => <div data-testid="message-content">{content}</div>,
}));

vi.mock('./ActivitySection', () => ({
  ActivitySection: ({ toolCalls }: { toolCalls: any[] }) => (
    <div data-testid="activity-section">{toolCalls.length} tool calls</div>
  ),
}));

vi.mock('./ModelConfigPopover', () => ({
  ModelConfigPopover: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}));

vi.mock('./ElicitationCard', () => ({
  ElicitationCard: () => <div data-testid="elicitation-card" />,
}));

// Mock clipboard utility
vi.mock('../utils/clipboard', () => ({
  copyToClipboard: vi.fn().mockResolvedValue(true),
}));

// Helper: Create Turn objects
function makeTurn(overrides: Partial<Turn> = {}): Turn {
  return {
    id: 'turn-0',
    userMessage: undefined,
    agentMessages: [],
    toolCalls: [],
    delegations: [],
    agentId: 'primary',
    startTime: 1000,
    endTime: 2000,
    isActive: false,
    ...overrides,
  };
}

// Helper: Create EventRow objects
function makeRow(overrides: Partial<EventRow> = {}): EventRow {
  return {
    id: 'row-1',
    type: 'agent',
    content: 'Hello',
    timestamp: 1000,
    depth: 0,
    isMessage: true,
    agentId: 'primary',
    ...overrides,
  } as EventRow;
}

describe('TurnCard', () => {
  const defaultAgents: UiAgentInfo[] = [
    { id: 'primary', name: 'Primary Agent', description: 'Primary agent for handling tasks', capabilities: [] },
  ];

  const defaultProps = {
    turn: makeTurn(),
    turnIndex: 0,
    agents: defaultAgents,
    onToolClick: vi.fn(),
    onDelegateClick: vi.fn(),
  };

  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('renders user message when turn has userMessage', () => {
    const turn = makeTurn({
      userMessage: makeRow({ type: 'user', content: 'Hello AI' }),
    });

    render(<TurnCard {...defaultProps} turn={turn} />);

    expect(screen.getByText('User')).toBeInTheDocument();
    expect(screen.getByText('Hello AI')).toBeInTheDocument();
  });

  it('renders agent name from agents list', () => {
    const turn = makeTurn({ agentId: 'agent-1', agentMessages: [makeRow({ content: 'Hi' })] });
    const agents: UiAgentInfo[] = [{ id: 'agent-1', name: 'Code Agent', description: 'Code agent', capabilities: [] }];

    render(<TurnCard {...defaultProps} turn={turn} agents={agents} />);

    expect(screen.getByText('Code Agent')).toBeInTheDocument();
  });

  it('falls back to "Agent" when agentId is undefined', () => {
    const turn = makeTurn({ agentId: undefined, agentMessages: [makeRow({ content: 'Hi' })] });

    render(<TurnCard {...defaultProps} turn={turn} />);

    expect(screen.getByText('Agent')).toBeInTheDocument();
  });

  it('renders agent messages via MessageContent', () => {
    const turn = makeTurn({
      agentMessages: [makeRow({ content: 'Here is the answer' })],
    });

    render(<TurnCard {...defaultProps} turn={turn} />);

    expect(screen.getByText('Here is the answer')).toBeInTheDocument();
  });

  it('renders final thinking block for assistant messages', () => {
    const turn = makeTurn({
      agentMessages: [makeRow({ content: 'Answer body', thinking: 'reasoning trace' })],
    });

    render(<TurnCard {...defaultProps} turn={turn} />);

    expect(screen.getByText('Thinking')).toBeInTheDocument();
    expect(screen.getByText('reasoning trace')).toBeInTheDocument();
    expect(screen.getByText('Answer body')).toBeInTheDocument();
  });

  it('renders live thinking accumulator for streaming deltas', () => {
    const turn = makeTurn({
      agentMessages: [
        makeRow({
          content: '',
          thinking: 'live rationale',
          isStreamDelta: true,
          isThinkingDelta: true,
        }),
      ],
    });

    render(<TurnCard {...defaultProps} turn={turn} />);

    expect(screen.getByText('live rationale')).toBeInTheDocument();
  });

  it('renders "thinking..." when turn is active', () => {
    const turn = makeTurn({ isActive: true });

    render(<TurnCard {...defaultProps} turn={turn} />);

    expect(screen.getByText('thinking...')).toBeInTheDocument();
  });

  it('does NOT render "thinking..." when turn is inactive', () => {
    const turn = makeTurn({ isActive: false });

    render(<TurnCard {...defaultProps} turn={turn} />);

    expect(screen.queryByText('thinking...')).not.toBeInTheDocument();
  });

  it('renders tool calls via ActivitySection', () => {
    const turn = makeTurn({
      toolCalls: [makeRow({ type: 'tool_call' })],
    });

    render(<TurnCard {...defaultProps} turn={turn} />);

    expect(screen.getByTestId('activity-section')).toBeInTheDocument();
    expect(screen.getByText('1 tool calls')).toBeInTheDocument();
  });

  it('renders "Working..." when turn is active with no messages or tools', () => {
    const turn = makeTurn({
      isActive: true,
      agentMessages: [],
      toolCalls: [],
    });

    render(<TurnCard {...defaultProps} turn={turn} />);

    expect(screen.getByText('Working...')).toBeInTheDocument();
  });

  it('does NOT render agent response section when turn is inactive with no content', () => {
    const turn = makeTurn({
      isActive: false,
      agentMessages: [],
      toolCalls: [],
    });

    render(<TurnCard {...defaultProps} turn={turn} />);

    expect(screen.queryByText('Working...')).not.toBeInTheDocument();
    expect(screen.queryByText('Agent')).not.toBeInTheDocument();
  });

  it('shows model label when showModelLabel is true', () => {
    const turn = makeTurn({
      modelLabel: 'anthropic / claude-3',
      modelConfigId: 1,
      agentMessages: [makeRow({ content: 'Hi' })],
    });

    render(<TurnCard {...defaultProps} turn={turn} showModelLabel={true} />);

    expect(screen.getByText('anthropic / claude-3')).toBeInTheDocument();
  });

  it('hides model label when showModelLabel is false', () => {
    const turn = makeTurn({
      modelLabel: 'anthropic / claude-3',
      modelConfigId: 1,
    });

    render(<TurnCard {...defaultProps} turn={turn} showModelLabel={false} />);

    expect(screen.queryByText('anthropic / claude-3')).not.toBeInTheDocument();
  });

  it('shows undo button when canUndo is true and turn is not active', () => {
    const turn = makeTurn({
      isActive: false,
      userMessage: makeRow({ type: 'user', messageId: 'msg-undo-1' }),
      agentMessages: [makeRow({ content: 'Done' })],
    });

    render(<TurnCard {...defaultProps} turn={turn} canUndo={true} onUndoTurn={vi.fn()} />);

    expect(screen.getByText('Undo')).toBeInTheDocument();
  });

  it('hides undo button when turn is active', () => {
    const turn = makeTurn({
      isActive: true,
      userMessage: makeRow({ type: 'user', messageId: 'msg-active-1' }),
    });

    render(<TurnCard {...defaultProps} turn={turn} canUndo={true} onUndoTurn={vi.fn()} />);

    expect(screen.queryByText('Undo')).not.toBeInTheDocument();
  });

  it('shows collapsed "Turn undone" indicator when isUndone is true', () => {
    const turn = makeTurn();

    render(
      <TurnCard
        {...defaultProps}
        turn={turn}
        isUndone={true}
        revertedFiles={['src/foo.ts', 'src/bar.ts']}
        onRedo={vi.fn()}
      />
    );

    expect(screen.getByText('Turn undone')).toBeInTheDocument();
    expect(screen.getByText('2 files reverted')).toBeInTheDocument();
    expect(screen.getByText('Redo')).toBeInTheDocument();
  });

  it('shows file count in collapsed undone indicator', () => {
    const turn = makeTurn();
    const revertedFiles = [
      'src/a.ts',
      'src/b.ts',
      'src/c.ts',
      'src/d.ts',
      'src/e.ts',
      'src/f.ts',
    ];

    render(
      <TurnCard
        {...defaultProps}
        turn={turn}
        isUndone={true}
        revertedFiles={revertedFiles}
      />
    );

    expect(screen.getByText('6 files reverted')).toBeInTheDocument();
  });

  it('calls onUndoTurn when undo button clicked', async () => {
    const user = userEvent.setup();
    const onUndoTurn = vi.fn();
    const turn = makeTurn({
      isActive: false,
      userMessage: makeRow({ type: 'user', messageId: 'msg-onundo-1' }),
      agentMessages: [makeRow({ content: 'Done' })],
    });

    render(<TurnCard {...defaultProps} turn={turn} turnIndex={3} canUndo={true} onUndoTurn={onUndoTurn} />);

    const undoButton = screen.getByText('Undo').closest('button');
    expect(undoButton).toBeTruthy();
    await user.click(undoButton!);

    expect(onUndoTurn).toHaveBeenCalledTimes(1);
    expect(onUndoTurn).toHaveBeenCalledWith(3);
  });

  it('calls onRedo when redo button clicked in collapsed undone indicator', async () => {
    const user = userEvent.setup();
    const onRedo = vi.fn();
    const turn = makeTurn();

    render(
      <TurnCard
        {...defaultProps}
        turn={turn}
        isUndone={true}
        revertedFiles={['src/foo.ts']}
        onRedo={onRedo}
      />
    );

    const redoButton = screen.getByText('Redo').closest('button');
    expect(redoButton).toBeTruthy();
    await user.click(redoButton!);

    expect(onRedo).toHaveBeenCalledTimes(1);
  });

  it('disables redo button and shows spinner after click', async () => {
    const user = userEvent.setup();
    const onRedo = vi.fn();
    const turn = makeTurn();

    render(
      <TurnCard
        {...defaultProps}
        turn={turn}
        isUndone={true}
        revertedFiles={['src/foo.ts']}
        onRedo={onRedo}
      />
    );

    await user.click(screen.getByText('Redo').closest('button')!);

    // After click: button shows "Redoing" and is disabled
    expect(screen.getByText('Redoing')).toBeInTheDocument();
    expect(screen.getByText('Redoing…')).toBeInTheDocument();
    const disabledButton = screen.getByText('Redoing').closest('button');
    expect(disabledButton).toBeDisabled();

    // Second click should not fire
    await user.click(disabledButton!);
    expect(onRedo).toHaveBeenCalledTimes(1);
  });

  it('resets redo pending state on undo-redo-undo cycle', async () => {
    const user = userEvent.setup();
    const onRedo = vi.fn();
    const turn = makeTurn();

    const { rerender } = render(
      <TurnCard
        {...defaultProps}
        turn={turn}
        isUndone={true}
        revertedFiles={['src/foo.ts']}
        onRedo={onRedo}
      />
    );

    // Click redo — enters pending state
    await user.click(screen.getByText('Redo').closest('button')!);
    expect(screen.getByText('Redoing')).toBeInTheDocument();

    // Redo completes — turn is no longer undone
    rerender(
      <TurnCard
        {...defaultProps}
        turn={turn}
        isUndone={false}
        revertedFiles={[]}
        onRedo={onRedo}
      />
    );

    // User undoes again — isUndone flips back to true
    rerender(
      <TurnCard
        {...defaultProps}
        turn={turn}
        isUndone={true}
        revertedFiles={['src/foo.ts']}
        onRedo={onRedo}
      />
    );

    // Should show "Redo" (not stuck on "Redoing")
    expect(screen.getByText('Redo')).toBeInTheDocument();
    expect(screen.queryByText('Redoing')).not.toBeInTheDocument();
  });

  it('shows fork button when turn has a user message id and is eligible', () => {
    const turn = makeTurn({
      isActive: false,
      userMessage: makeRow({ type: 'user', messageId: 'msg-fork-1' }),
      agentMessages: [makeRow({ content: 'Done' })],
    });

    render(<TurnCard {...defaultProps} turn={turn} onForkTurn={vi.fn()} />);

    expect(screen.getByText('Fork')).toBeInTheDocument();
  });

  it('shows fork button when turn has assistant message id and no user message id', () => {
    const turn = makeTurn({
      isActive: false,
      userMessage: makeRow({ type: 'user', messageId: undefined }),
      agentMessages: [makeRow({ type: 'agent', messageId: 'msg-fork-agent-1' })],
    });

    render(<TurnCard {...defaultProps} turn={turn} onForkTurn={vi.fn()} />);

    expect(screen.getByText('Fork')).toBeInTheDocument();
  });

  it('calls onForkTurn when fork button clicked', async () => {
    const user = userEvent.setup();
    const onForkTurn = vi.fn();
    const turn = makeTurn({
      isActive: false,
      userMessage: makeRow({ type: 'user', messageId: 'msg-fork-2' }),
      agentMessages: [makeRow({ content: 'Done' })],
    });

    render(<TurnCard {...defaultProps} turn={turn} turnIndex={5} onForkTurn={onForkTurn} />);

    const forkButton = screen.getByText('Fork').closest('button');
    expect(forkButton).toBeTruthy();
    await user.click(forkButton!);

    expect(onForkTurn).toHaveBeenCalledTimes(1);
    expect(onForkTurn).toHaveBeenCalledWith(5);
  });

  // ==================== Edge Case Tests ====================

  it('undo button hidden when canUndo false', () => {
    const turn = makeTurn({
      isActive: false,
      userMessage: makeRow({ type: 'user', messageId: 'msg-cannot-undo-1' }),
    });

    render(<TurnCard {...defaultProps} turn={turn} canUndo={false} />);

    expect(screen.queryByText('Undo')).not.toBeInTheDocument();
  });

  it('collapsed undone indicator does not show redo when onRedo not provided', () => {
    const turn = makeTurn();

    render(
      <TurnCard
        {...defaultProps}
        turn={turn}
        isUndone={true}
        revertedFiles={['src/foo.ts']}
        // No onRedo callback
      />
    );

    expect(screen.getByText('Turn undone')).toBeInTheDocument();
    expect(screen.queryByText('Redo')).not.toBeInTheDocument();
  });

  it('collapsed undone indicator with 0 reverted files does not crash', () => {
    const turn = makeTurn();

    render(
      <TurnCard
        {...defaultProps}
        turn={turn}
        isUndone={true}
        revertedFiles={[]}
      />
    );

    expect(screen.getByText('Turn undone')).toBeInTheDocument();
    expect(screen.getByText('no file changes')).toBeInTheDocument();
  });

  it('undo button hidden when isUndone is true', () => {
    const turn = makeTurn({ isActive: false });

    render(
      <TurnCard
        {...defaultProps}
        turn={turn}
        canUndo={true}
        isUndone={true}
        revertedFiles={['src/foo.ts']}
        onUndoTurn={vi.fn()}
      />
    );

    // Can't undo an already undone turn
    expect(screen.queryByText('Undo')).not.toBeInTheDocument();
  });

  it('shows stacked undo placeholder without redo action', () => {
    const turn = makeTurn({ isActive: false });

    render(
      <TurnCard
        {...defaultProps}
        turn={turn}
        isStackedUndone={true}
        onRedo={vi.fn()}
      />
    );

    expect(screen.getByText('Undone in stack. Redo newer undo first.')).toBeInTheDocument();
    expect(screen.queryByText('Redo Changes')).not.toBeInTheDocument();
  });

  it('dims turn content when stacked undone', () => {
    const turn = makeTurn({ isActive: false });

    const { container } = render(
      <TurnCard
        {...defaultProps}
        turn={turn}
        isStackedUndone={true}
      />
    );

    const root = container.querySelector('.turn-card');
    expect(root).toHaveAttribute('data-stacked-undone', 'true');
    expect(root).toHaveClass('opacity-45');
  });

  it('shows pending undo overlay and hides redo action', () => {
    const turn = makeTurn({ isActive: false });

    render(
      <TurnCard
        {...defaultProps}
        turn={turn}
        isUndoPending={true}
        onRedo={vi.fn()}
      />
    );

    expect(screen.getByText('Undoing Changes...')).toBeInTheDocument();
    expect(screen.queryByText('Redo Changes')).not.toBeInTheDocument();
  });

  it('undo button hidden when turn is stacked undone', () => {
    const turn = makeTurn({ isActive: false });

    render(
      <TurnCard
        {...defaultProps}
        turn={turn}
        canUndo={true}
        isStackedUndone={true}
        onUndoTurn={vi.fn()}
      />
    );

    expect(screen.queryByText('Undo')).not.toBeInTheDocument();
  });

  it('undo button hidden when top undo is pending', () => {
    const turn = makeTurn({ isActive: false });

    render(
      <TurnCard
        {...defaultProps}
        turn={turn}
        canUndo={true}
        isUndoPending={true}
        onUndoTurn={vi.fn()}
      />
    );

    expect(screen.queryByText('Undo')).not.toBeInTheDocument();
  });

  describe('mobile overflow prevention', () => {
    it('turn-card wrapper has overflow-hidden to prevent horizontal bleed', () => {
      const turn = makeTurn({
        userMessage: { content: 'Hello', timestamp: 1000 } as any,
        agentMessages: [{ id: 'a1', type: 'agent' as const, content: 'Hi', timestamp: 2000, agentId: 'primary', isMessage: true }] as any,
      });

      const { container } = render(
        <TurnCard {...defaultProps} turn={turn} />
      );

      const turnCard = container.querySelector('.turn-card') as HTMLElement;
      expect(turnCard).not.toBeNull();
      expect(turnCard.className).toContain('overflow-hidden');
    });

    it('turn-card uses responsive horizontal padding', () => {
      const turn = makeTurn({
        userMessage: { content: 'Hello', timestamp: 1000 } as any,
        agentMessages: [{ id: 'a1', type: 'agent' as const, content: 'Hi', timestamp: 2000, agentId: 'primary', isMessage: true }] as any,
      });

      const { container } = render(
        <TurnCard {...defaultProps} turn={turn} />
      );

      const turnCard = container.querySelector('.turn-card') as HTMLElement;
      expect(turnCard).not.toBeNull();
      // Should have responsive padding: smaller on mobile, normal on desktop
      expect(turnCard.className).toContain('px-2');
      expect(turnCard.className).toContain('md:px-4');
    });
  });
});
