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

import { historyWithApiAttachmentContext } from '../src/api-attachment-context';
import {
  fetchProjectFilePreview,
  fetchProjectFileText,
} from '../src/providers/registry';
import type { ChatMessage, ProjectFile } from '../src/types';

vi.mock('../src/providers/registry', async () => {
  const actual = await vi.importActual<typeof import('../src/providers/registry')>(
    '../src/providers/registry',
  );
  return {
    ...actual,
    fetchProjectFilePreview: vi.fn().mockResolvedValue(null),
    fetchProjectFileText: vi.fn().mockResolvedValue(null),
  };
});

const mockedFetchProjectFilePreview = vi.mocked(fetchProjectFilePreview);
const mockedFetchProjectFileText = vi.mocked(fetchProjectFileText);

describe('historyWithApiAttachmentContext', () => {
  afterEach(() => {
    vi.clearAllMocks();
  });

  it('adds extracted document previews to the target user message', async () => {
    mockedFetchProjectFilePreview.mockResolvedValue({
      kind: 'document',
      title: 'brief.docx',
      sections: [{ title: 'Document', lines: ['Hello world', 'Second line'] }],
    });

    const history = await historyWithApiAttachmentContext(
      [userMessage('msg-1', 'Summarize this', [{ path: 'brief.docx', name: 'brief.docx', kind: 'file' }])],
      'msg-1',
      'project-1',
      [projectFile('brief.docx', 'document')],
    );

    expect(mockedFetchProjectFilePreview).toHaveBeenCalledWith('project-1', 'brief.docx');
    expect(history[0]?.content).toContain('<attached-project-files>');
    expect(history[0]?.content).toContain('user-visible order');
    expect(history[0]?.content).toContain('### Attachment 1: brief.docx');
    expect(history[0]?.content).toContain('Hello world');
    expect(history[0]?.content).toContain('Second line');
  });

  it('preserves uploaded attachment order with numbered headings', async () => {
    const history = await historyWithApiAttachmentContext(
      [
        userMessage('msg-1', 'Compare the first and second image', [
          { path: 'uploads/first.png', name: 'image.png', kind: 'image' },
          { path: 'uploads/second.png', name: 'image.png', kind: 'image' },
        ]),
      ],
      'msg-1',
      'project-1',
      [projectFile('uploads/first.png', 'image'), projectFile('uploads/second.png', 'image')],
    );

    const content = history[0]?.content ?? '';
    expect(content.indexOf('### Attachment 1:')).toBeLessThan(content.indexOf('### Attachment 2:'));
    expect(content).toContain('path: uploads/first.png');
    expect(content).toContain('path: uploads/second.png');
  });

  it('uses the explicit user-visible order before numbering attachments', async () => {
    const history = await historyWithApiAttachmentContext(
      [
        userMessage('msg-1', 'Compare the first and second image', [
          { path: 'uploads/second.png', name: 'second.png', kind: 'image', order: 1 },
          { path: 'uploads/first.png', name: 'first.png', kind: 'image', order: 0 },
        ]),
      ],
      'msg-1',
      'project-1',
      [projectFile('uploads/first.png', 'image'), projectFile('uploads/second.png', 'image')],
    );

    const content = history[0]?.content ?? '';
    expect(content.indexOf('### Attachment 1: first.png')).toBeLessThan(
      content.indexOf('### Attachment 2: second.png'),
    );
  });

  it('reads raw text attachments with a cache buster from file metadata', async () => {
    mockedFetchProjectFileText.mockResolvedValue('const answer = 42;');

    const history = await historyWithApiAttachmentContext(
      [userMessage('msg-1', 'Use this code', [{ path: 'src/demo.ts', name: 'demo.ts', kind: 'file' }])],
      'msg-1',
      'project-1',
      [projectFile('src/demo.ts', 'code')],
    );

    expect(mockedFetchProjectFileText).toHaveBeenCalledWith(
      'project-1',
      'src/demo.ts',
      { cache: 'no-store', cacheBustKey: 123 },
    );
    expect(history[0]?.content).toContain('```ts');
    expect(history[0]?.content).toContain('const answer = 42;');
  });

  it('does not fetch raw text for sketch image attachments', async () => {
    const history = await historyWithApiAttachmentContext(
      [userMessage('msg-1', 'Use this sketch', [{ path: 'sketch-board.png', name: 'sketch-board.png', kind: 'image' }])],
      'msg-1',
      'project-1',
      [projectFile('sketch-board.png', 'sketch')],
    );

    expect(mockedFetchProjectFileText).not.toHaveBeenCalled();
    expect(mockedFetchProjectFilePreview).not.toHaveBeenCalled();
    expect(history[0]?.content).toContain('kind: sketch');
    expect(history[0]?.content).toContain('Content preview unavailable');
  });

  it('omits image attachment metadata when the provider sends native image blocks', async () => {
    for (const path of ['hero.png', 'hero.jpg', 'hero.jpeg', 'hero.gif', 'hero.webp']) {
      const history = await historyWithApiAttachmentContext(
        [
          userMessage('msg-1', 'Describe this image', [
            { path, name: path, kind: 'image' },
          ]),
        ],
        'msg-1',
        'project-1',
        [projectFile(path, 'image')],
        { omitNativeImageAttachments: true },
      );

      expect(history[0]?.content).toBe('Describe this image');
    }
    expect(mockedFetchProjectFileText).not.toHaveBeenCalled();
    expect(mockedFetchProjectFilePreview).not.toHaveBeenCalled();
  });

  it('omits sketch-prefixed raster image metadata when native image blocks carry them', async () => {
    const history = await historyWithApiAttachmentContext(
      [
        userMessage('msg-1', 'Describe this image', [
          { path: 'sketch-hero.png', name: 'sketch-hero.png', kind: 'image' },
        ]),
      ],
      'msg-1',
      'project-1',
      [projectFile('sketch-hero.png', 'sketch')],
      { omitNativeImageAttachments: true },
    );

    expect(history[0]?.content).toBe('Describe this image');
    expect(mockedFetchProjectFileText).not.toHaveBeenCalled();
    expect(mockedFetchProjectFilePreview).not.toHaveBeenCalled();
  });

  it('keeps unsupported image metadata when native image blocks cannot carry them', async () => {
    for (const path of ['hero.avif', 'hero.bmp']) {
      const history = await historyWithApiAttachmentContext(
        [
          userMessage('msg-1', 'Describe this image', [
            { path, name: path, kind: 'image' },
          ]),
        ],
        'msg-1',
        'project-1',
        [projectFile(path, 'image')],
        { omitNativeImageAttachments: true },
      );

      expect(history[0]?.content).toContain('<attached-project-files>');
      expect(history[0]?.content).toContain(`path: ${path}`);
      expect(history[0]?.content).toContain('Content preview unavailable');
    }
  });

  it('uses filename inference when the project file list has not refreshed yet', async () => {
    mockedFetchProjectFilePreview.mockResolvedValue({
      kind: 'pdf',
      title: 'report.pdf',
      sections: [{ title: 'PDF', lines: ['Quarterly results'] }],
    });

    const history = await historyWithApiAttachmentContext(
      [userMessage('msg-1', 'Read this', [{ path: 'report.pdf', name: 'report.pdf', kind: 'file' }])],
      'msg-1',
      'project-1',
      [],
    );

    expect(mockedFetchProjectFilePreview).toHaveBeenCalledWith('project-1', 'report.pdf');
    expect(history[0]?.content).toContain('Quarterly results');
  });
});

function userMessage(
  id: string,
  content: string,
  attachments: NonNullable<ChatMessage['attachments']>,
): ChatMessage {
  return {
    id,
    role: 'user',
    content,
    createdAt: 1,
    attachments,
  };
}

function projectFile(path: string, kind: ProjectFile['kind']): ProjectFile {
  return {
    name: path.split('/').pop() ?? path,
    path,
    type: 'file',
    size: 100,
    mtime: 123,
    kind,
    mime: 'application/octet-stream',
  };
}
