import { describe, expect, it, vi } from 'vitest';
import { fireEvent, render, screen } from '@testing-library/react';
import { ChatMessage } from '@/pages/Chat/ChatMessage';
import { getToolVisibilityId } from '@/pages/Chat/chat-transcript';
import { formatTimestamp } from '@/pages/Chat/message-utils';
import type { RawMessage } from '@/types/chat';

vi.mock('@/lib/api-client', () => ({
  invokeIpc: vi.fn(),
  statFile: vi.fn(async (path: string) => {
    if (path.includes('missing')) {
      return { ok: false, error: 'notFound' };
    }
    const isFile = /\.[A-Za-z0-9]+$/.test(path);
    return {
      ok: true,
      isFile,
      isDir: !isFile,
      size: isFile ? 1024 : 0,
    };
  }),
}));

describe('ChatMessage attachment rendering', () => {
  it('keeps attachment-only assistant replies visible when process attachments are suppressed', () => {
    const message: RawMessage = {
      role: 'assistant',
      content: [],
      _attachedFiles: [
        {
          fileName: 'artifact.png',
          mimeType: 'image/png',
          fileSize: 0,
          preview: '/tmp/artifact.png',
          filePath: '/tmp/artifact.png',
          source: 'tool-result',
        },
      ],
    };

    render(<ChatMessage message={message} suppressProcessAttachments />);

    expect(screen.getByAltText('artifact.png')).toBeInTheDocument();
  });

  it('keeps image artifacts visible alongside reply text', () => {
    const message: RawMessage = {
      role: 'assistant',
      content: 'Screenshot captured.',
      _attachedFiles: [
        {
          fileName: 'desktop_screenshot.jpg',
          mimeType: 'image/jpeg',
          fileSize: 837_000,
          preview: 'data:image/jpeg;base64,xyz',
          filePath: '/tmp/desktop_screenshot.jpg',
          source: 'message-ref',
        },
      ],
    };

    render(<ChatMessage message={message} suppressProcessAttachments />);

    expect(screen.getByAltText('desktop_screenshot.jpg')).toBeInTheDocument();
  });

  it('hides metadata-only image attachments instead of rendering blank file tiles', () => {
    const message: RawMessage = {
      role: 'assistant',
      content: 'Screenshot captured.',
      _attachedFiles: [
        {
          fileName: 'desktop_screenshot.jpg',
          mimeType: 'image/jpeg',
          fileSize: 0,
          preview: null,
          filePath: '/tmp/desktop_screenshot.jpg',
          source: 'tool-result',
        },
      ],
    };

    render(<ChatMessage message={message} />);

    expect(screen.queryByAltText('desktop_screenshot.jpg')).not.toBeInTheDocument();
    expect(screen.queryByText('desktop_screenshot.jpg')).not.toBeInTheDocument();
  });

  it('deduplicates image attachments and keeps the copy with a preview', () => {
    const message: RawMessage = {
      role: 'user',
      content: 'Please inspect this screenshot.',
      _attachedFiles: [
        {
          fileName: 'farm.png',
          mimeType: 'image/png',
          fileSize: 1024,
          preview: null,
          filePath: '/tmp/farm.png',
          source: 'runtime-media',
        },
        {
          fileName: 'farm.png',
          mimeType: 'image/png',
          fileSize: 1024,
          preview: 'data:image/png;base64,xyz',
          filePath: '/tmp/farm.png',
          source: 'user-upload',
        },
      ],
    };

    render(<ChatMessage message={message} />);

    expect(screen.getAllByAltText('farm.png')).toHaveLength(1);
  });

  it('derives preview cards from assistant text paths under NianCode data', async () => {
    const message: RawMessage = {
      role: 'assistant',
      content: 'Generated file: `/Users/me/.niancode/workspace/report.pdf`',
    };

    render(<ChatMessage message={message} suppressProcessAttachments />);

    expect(await screen.findByText('report.pdf')).toBeInTheDocument();
  });

  it('derives skill directory cards from .niancode skill paths', async () => {
    const message: RawMessage = {
      role: 'assistant',
      content: 'Skill location: ~/.niancode/skills/open-eastmoney',
    };

    render(<ChatMessage message={message} suppressProcessAttachments />);

    expect(await screen.findByText('open-eastmoney')).toBeInTheDocument();
    expect(screen.getByText('Folder')).toBeInTheDocument();
  });

  it('does not show cards for missing text paths', async () => {
    const message: RawMessage = {
      role: 'assistant',
      content: 'Missing file: ~/.niancode/skills/missing-skill/SKILL.md',
    };

    render(<ChatMessage message={message} suppressProcessAttachments />);

    await new Promise((resolve) => setTimeout(resolve, 0));
    expect(screen.queryByText('SKILL.md')).not.toBeInTheDocument();
  });

  it('continues hiding non-preview process attachments when process attachments are suppressed', () => {
    const message: RawMessage = {
      role: 'assistant',
      content: 'I also used a temporary file.',
      _attachedFiles: [
        {
          fileName: 'debug.log',
          mimeType: 'text/plain',
          fileSize: 1024,
          preview: null,
          filePath: '/tmp/debug.log',
          source: 'message-ref',
        },
      ],
    };

    render(<ChatMessage message={message} suppressProcessAttachments />);

    expect(screen.queryByText('debug.log')).not.toBeInTheDocument();
  });
});

describe('ChatMessage delivery status rendering', () => {
  it('shows queued status for a pending user prompt', () => {
    const message = {
      role: 'user',
      content: [{ type: 'text', text: 'Run the next task' }],
      _deliveryStatus: 'queued',
    } as RawMessage;

    render(<ChatMessage message={message} />);

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

  it('shows sending status for an active user prompt', () => {
    const message = {
      role: 'user',
      content: [{ type: 'text', text: 'Run this now' }],
      _deliveryStatus: 'sending',
    } as RawMessage;

    render(<ChatMessage message={message} />);

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

describe('ChatMessage current-view controls', () => {
  it('hides both user and assistant timestamps when showTimestamps is false', () => {
    const timestamp = Date.UTC(2026, 6, 18, 8, 0, 0);
    const { rerender } = render(
      <ChatMessage message={{ role: 'user', content: 'hello', timestamp }} showTimestamps={false} />,
    );
    expect(screen.queryByText(formatTimestamp(timestamp))).not.toBeInTheDocument();

    rerender(
      <ChatMessage message={{ role: 'assistant', content: 'world', timestamp }} showTimestamps={false} />,
    );
    expect(screen.queryByText(formatTimestamp(timestamp))).not.toBeInTheDocument();
  });

  it('renders tool details from the parent-controlled expanded ID set', () => {
    const message: RawMessage = {
      id: 'assistant-tools',
      role: 'assistant',
      content: [{
        type: 'tool_use',
        id: 'tool-read',
        name: 'read',
        input: { file: 'README.md' },
      }],
    };
    const onToolExpandedChange = vi.fn();
    const toolId = getToolVisibilityId('assistant-tools', 'tool-read', 0);
    const { rerender } = render(
      <ChatMessage
        message={message}
        messageKey="assistant-tools"
        expandedToolIds={new Set()}
        onToolExpandedChange={onToolExpandedChange}
      />,
    );

    expect(screen.getByText(/README\.md/).closest('[data-state]')).toHaveAttribute('data-state', 'closed');
    fireEvent.click(screen.getByRole('button', { name: /read/i }));
    expect(onToolExpandedChange).toHaveBeenCalledWith(toolId, true);

    rerender(
      <ChatMessage
        message={message}
        messageKey="assistant-tools"
        expandedToolIds={new Set([toolId])}
        onToolExpandedChange={onToolExpandedChange}
      />,
    );
    expect(screen.getByText(/README\.md/)).toBeInTheDocument();
  });
});

describe('ChatMessage bubble surface styling', () => {
  it('uses the Makelore light paper palette for user and assistant message backgrounds', () => {
    const userMessage: RawMessage = {
      role: 'user',
      content: [{ type: 'text', text: 'Please inspect this prototype.' }],
    };
    const assistantMessage: RawMessage = {
      role: 'assistant',
      content: 'I will review it against the project style.',
    };

    const { rerender } = render(<ChatMessage message={userMessage} />);

    expect(screen.getByTestId('chat-message-bubble-user')).toHaveClass(
      'chat-user-message-surface',
      'bg-brand-soft',
      'text-foreground',
      'shadow-soft',
    );
    expect(screen.getByTestId('chat-message-bubble-user')).not.toHaveClass('bg-[#168fff]', 'text-white');

    rerender(<ChatMessage message={assistantMessage} />);

    expect(screen.getByTestId('chat-message-bubble-assistant')).toHaveClass(
      'chat-assistant-message-surface',
      'bg-background',
      'text-foreground',
      'shadow-soft',
    );
  });

  it('does not render a user avatar or an extra row gap', () => {
    const message: RawMessage = {
      role: 'user',
      content: 'Please inspect this prototype.',
    };

    render(<ChatMessage message={message} />);

    expect(screen.queryByTestId('chat-message-avatar-user')).not.toBeInTheDocument();
    expect(screen.getByTestId('chat-message-bubble-user').parentElement?.parentElement).toHaveClass('gap-0');
  });
});

describe('ChatMessage LaTeX rendering', () => {
  it('renders the configured project Agent avatar for assistant messages', () => {
    const message: RawMessage = { role: 'assistant', content: '已完成。' };
    render(<ChatMessage message={message} assistantAvatarSrc="/avatar-16.png" assistantAvatarAlt="小码头像" />);
    expect(screen.getByRole('img', { name: '小码头像' })).toHaveAttribute('src', '/avatar-16.png');
  });

  it('reserves the assistant column without painting a second avatar', () => {
    const message: RawMessage = { role: 'assistant', content: '最终回答。' };
    render(<ChatMessage message={message} assistantAvatarSrc="/avatar-16.png" assistantAvatarMode="reserved" />);

    expect(screen.queryByTestId('chat-message-avatar-assistant')).not.toBeInTheDocument();
    expect(screen.getByTestId('chat-message-avatar-reserved')).toHaveAttribute('aria-hidden', 'true');
    expect(screen.queryByRole('img')).not.toBeInTheDocument();
  });

  it('compacts the assistant reply that follows an execution trace', () => {
    const message: RawMessage = { role: 'assistant', content: '最终回答。' };
    render(
      <ChatMessage
        message={message}
        assistantAvatarMode="reserved-trace"
        compactAssistantReply
      />,
    );

    expect(screen.getByTestId('chat-message-bubble-assistant')).toHaveClass('pt-1', 'pb-3');
  });

  it('aligns an embedded execution-trace reply without reserving a duplicate avatar', () => {
    const message: RawMessage = { role: 'assistant', content: '最终回答。' };
    render(
      <ChatMessage
        message={message}
        assistantAvatarSrc="/avatar-16.png"
        assistantAvatarMode="embedded-trace"
      />,
    );

    expect(screen.queryByTestId('chat-message-avatar-assistant')).not.toBeInTheDocument();
    expect(screen.queryByTestId('chat-message-avatar-reserved')).not.toBeInTheDocument();
    expect(screen.getByTestId('chat-message-bubble-assistant').parentElement).not.toHaveClass('pl-9');
  });

  it('marks assistant markdown with compact paragraph spacing', () => {
    render(
      <ChatMessage
        message={{ role: 'assistant', content: '第一段。\n\n第二段。' }}
      />,
    );

    expect(screen.getByTestId('chat-message-bubble-assistant').querySelector('.chat-assistant-message-markdown')).toHaveClass('prose', 'prose-sm');
  });

  it('does not render markdown thematic breaks inside assistant replies', () => {
    const { container } = render(
      <ChatMessage
        message={{ role: 'assistant', content: '第一段。\n\n---\n\n第二段。' }}
      />,
    );

    expect(container.querySelector('hr')).not.toBeInTheDocument();
    expect(screen.getByText('第一段。')).toBeInTheDocument();
    expect(screen.getByText('第二段。')).toBeInTheDocument();
  });

  it('renders inline `$...$` math with KaTeX', () => {
    const message: RawMessage = {
      role: 'assistant',
      content: 'Mass-energy equivalence: $E=mc^2$ is famous.',
    };
    const { container } = render(<ChatMessage message={message} />);
    expect(container.querySelector('.katex')).not.toBeNull();
  });

  it('renders display `$$...$$` math as a block', () => {
    const message: RawMessage = {
      role: 'assistant',
      content: 'Definite integral:\n\n$$\n\\int_0^1 x\\,dx = \\frac{1}{2}\n$$\n',
    };
    const { container } = render(<ChatMessage message={message} />);
    expect(container.querySelector('.katex-display')).not.toBeNull();
  });

  it('renders `\\(...\\)` inline math', () => {
    const message: RawMessage = {
      role: 'assistant',
      content: 'Quadratic formula: \\(x = \\frac{-b \\pm \\sqrt{b^2-4ac}}{2a}\\).',
    };
    const { container } = render(<ChatMessage message={message} />);
    expect(container.querySelector('.katex')).not.toBeNull();
    expect(container.querySelector('.katex-display')).toBeNull();
  });

  it('does not rewrite `\\(` inside code fences', () => {
    const message: RawMessage = {
      role: 'assistant',
      content: 'Code sample:\n\n```\nprintf("\\(hello\\)")\n```\n',
    };
    const { container } = render(<ChatMessage message={message} />);
    expect(container.textContent).toContain('\\(hello\\)');
    expect(container.querySelector('.katex')).toBeNull();
  });
});
