// @vitest-environment jsdom
/**
 * Component-render tests for FilePreview's per-type body branches:
 *  - markdown  → rendered HTML; a RELATIVE image href is rewritten to the
 *                workspace raw-file endpoint via resolvePreviewImageHref
 *                (the pure helper itself is tested in lib/filePreviewPath.test.ts;
 *                here we assert the COMPONENT actually wires it into the <img src>).
 *  - csv       → an HTML <table> with one <td> per cell
 *  - jsonl     → renders rows (does not crash on structured lines)
 *  - unknown   → falls back to a <pre> with the raw content
 *
 * Heavy/irrelevant deps are mocked: mermaid (async diagram run), the office API,
 * and EmbedBlock. i18n uses the real instance for the 'files' namespace.
 */
import '../../test/dom-setup';
import i18n from '../../i18n';
import { describe, it, expect, vi, beforeAll } from 'vitest';
import { screen, within } from '@testing-library/react';
import { render } from '@testing-library/react';

// office プレビューのラベルは翻訳されるので、テストは ja に固定して日本語文言で検証する。
beforeAll(async () => { await i18n.changeLanguage('ja'); });

// jsdom has no IntersectionObserver; MarkdownPreview's scroll-spy effect uses
// one when the doc has headings. Provide a no-op stub so the effect is inert.
beforeAll(() => {
  if (!('IntersectionObserver' in globalThis)) {
    class IO {
      observe() {}
      unobserve() {}
      disconnect() {}
      takeRecords() { return []; }
    }
    // @ts-expect-error test stub
    globalThis.IntersectionObserver = IO;
  }
});

// mermaid.run is fire-and-forget inside a useEffect; stub it so jsdom does not
// choke on diagram rendering.
vi.mock('mermaid', () => ({
  default: { initialize: vi.fn(), run: vi.fn().mockResolvedValue(undefined) },
}));

// EmbedBlock fetches structured blocks; the markdown we feed has no embeds, but
// stub it anyway to keep the module graph light and network-free.
vi.mock('../embed/EmbedBlock', () => ({ EmbedBlock: () => null }));

// Avoid pulling real network code from the office-preview path.
vi.mock('../../api', async (importOriginal) => {
  const actual = await importOriginal<typeof import('../../api')>();
  return {
    ...actual,
    fetchOfficePreview: vi.fn(),
    updateLocalFileContent: vi.fn(),
  };
});

import { FilePreview, MarkdownPreview } from './FilePreview';
import { fetchOfficePreview } from '../../api';

const noop = () => {};

describe('MarkdownPreview raw-HTML handling', () => {
  it('file preview (default) renders embedded raw HTML as live elements', () => {
    // Opening a .md file that intentionally embeds HTML should keep rendering
    // it — this is the existing behavior we must NOT regress.
    const { container } = render(
      <MarkdownPreview content={'text\n\n<div class="hi">boxed</div>'} />,
    );
    expect(container.querySelector('div.hi')?.textContent).toBe('boxed');
  });

  it('escapeRawHtml renders raw HTML as escaped source, not a live element', () => {
    // The chat result bubble passes escapeRawHtml so an HTML final answer does
    // not inject its own tags and break the bubble layout.
    const { container } = render(
      <MarkdownPreview escapeRawHtml content={'text\n\n<div class="hi">boxed</div>'} />,
    );
    expect(container.querySelector('div.hi')).toBeNull();
    expect(container.textContent).toContain('<div class="hi">boxed</div>');
  });

  it('escapeRawHtml keeps normal Markdown (headings, fenced code) working', () => {
    const { container } = render(
      <MarkdownPreview escapeRawHtml content={'# Hi\n\n```js\nconst a = 1;\n```'} />,
    );
    expect(container.querySelector('h1')?.textContent).toContain('Hi');
    expect(container.querySelector('pre code')).not.toBeNull();
  });
});

describe('FilePreview body branches', () => {
  it('markdown: renders HTML and rewrites a relative image href to the raw endpoint', () => {
    const base = '/api/local/tasks/42/files/raw?path=';
    const md = '# Title\n\n![alt text](images/pic.png)\n';
    render(
      <FilePreview
        name="report.md"
        content={md}
        markdownImageBaseUrl={base}
        onClose={noop}
      />,
    );
    // heading rendered as HTML
    expect(screen.getByRole('heading', { name: /Title/ })).toBeInTheDocument();
    // the relative image is rewritten through resolvePreviewImageHref
    const img = screen.getByAltText('alt text') as HTMLImageElement;
    expect(img.getAttribute('src')).toBe(`${base}${encodeURIComponent('images/pic.png')}`);
  });

  it('markdown: leaves an absolute image href untouched', () => {
    const md = '![remote](https://example.com/x.png)';
    render(
      <FilePreview
        name="a.md"
        content={md}
        markdownImageBaseUrl="/api/local/tasks/1/files/raw?path="
        onClose={noop}
      />,
    );
    const img = screen.getByAltText('remote') as HTMLImageElement;
    expect(img.getAttribute('src')).toBe('https://example.com/x.png');
  });

  it('csv: renders a table with one cell per value', () => {
    const csv = 'name,age\nalice,30\nbob,25';
    render(<FilePreview name="data.csv" content={csv} onClose={noop} />);
    const table = screen.getByRole('table');
    const cells = within(table).getAllByRole('cell');
    // 3 rows x 2 cols = 6 cells
    expect(cells).toHaveLength(6);
    expect(within(table).getByText('alice')).toBeInTheDocument();
    expect(within(table).getByText('25')).toBeInTheDocument();
  });

  it('jsonl: renders structured rows without crashing', () => {
    const jsonl = [
      JSON.stringify({ tool: 'WebFetch', outcome: 'ok' }),
      JSON.stringify({ tool: 'Bash', outcome: 'failed' }),
    ].join('\n');
    render(<FilePreview name="log.jsonl" content={jsonl} onClose={noop} />);
    // values from the jsonl appear somewhere in the rendered output
    expect(screen.getByText(/WebFetch/)).toBeInTheDocument();
    expect(screen.getByText(/Bash/)).toBeInTheDocument();
  });

  it('unknown type: falls back to a <pre> with the raw content', () => {
    render(<FilePreview name="notes.xyz" content="just some plain text" onClose={noop} />);
    expect(screen.getByText('just some plain text')).toBeInTheDocument();
    expect(screen.getByText('just some plain text').tagName).toBe('PRE');
  });

  it('renders the filename in the header and wires the close button', () => {
    render(<FilePreview name="report.md" content="# Hi" onClose={noop} />);
    expect(screen.getByTitle('report.md')).toBeInTheDocument();
    expect(screen.getByRole('dialog')).toBeInTheDocument();
  });

  it('office document: renders each page as an image with a ページ label', async () => {
    vi.mocked(fetchOfficePreview).mockResolvedValueOnce({
      kind: 'document',
      pages: [
        { index: 1, dataUrl: 'data:image/png;base64,AAA' },
        { index: 2, dataUrl: 'data:image/png;base64,BBB' },
      ],
      pageCount: 2,
      truncated: false,
    });
    render(
      <FilePreview
        name="report.docx"
        content=""
        office={{ kind: 'document', url: '/api/x/office-preview', downloadUrl: '/api/x/raw' }}
        onClose={noop}
      />,
    );
    const page1 = (await screen.findByAltText('ページ 1')) as HTMLImageElement;
    expect(page1.getAttribute('src')).toBe('data:image/png;base64,AAA');
    expect(screen.getByAltText('ページ 2')).toBeInTheDocument();
  });

  it('office document: shows the truncation note when truncated', async () => {
    vi.mocked(fetchOfficePreview).mockResolvedValueOnce({
      kind: 'document',
      pages: [{ index: 1, dataUrl: 'data:image/png;base64,AAA' }],
      pageCount: 60,
      truncated: true,
    });
    render(
      <FilePreview
        name="long.docx"
        content=""
        office={{ kind: 'document', url: '/api/x/office-preview' }}
        onClose={noop}
      />,
    );
    await screen.findByAltText('ページ 1');
    // "先頭 1 ページのみ表示しています（全 60 ページ）。"
    expect(screen.getByText(/全 60 ページ/)).toBeInTheDocument();
  });
});
