import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';

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

import {
  createAnnotationSourceMarkdownPlugin,
  preprocessAnnotationSourceMarkdown,
  resolveAnnotationMarkdownPath,
} from './annotationSourceMarkdown';

const tempRoots: string[] = [];

function createProjectRoot(): string {
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'annotation-source-markdown-'));
  tempRoots.push(root);
  fs.mkdirSync(path.join(root, 'src/prototypes/order-review/docs/nested'), { recursive: true });
  fs.writeFileSync(path.join(root, 'src/prototypes/order-review/index.tsx'), 'export default function Demo() { return null; }\n', 'utf8');
  return root;
}

function writeSource(projectRoot: string, source: unknown): string {
  const sourcePath = path.join(projectRoot, 'src/prototypes/order-review/annotation-source.json');
  fs.mkdirSync(path.dirname(sourcePath), { recursive: true });
  fs.writeFileSync(sourcePath, `${JSON.stringify(source, null, 2)}\n`, 'utf8');
  return sourcePath;
}

afterEach(() => {
  vi.restoreAllMocks();
  for (const root of tempRoots.splice(0)) {
    fs.rmSync(root, { recursive: true, force: true });
  }
});

describe('annotation source markdown preprocessing', () => {
  it('inlines directory markdownPath content without creating Make-specific edit URLs', () => {
    const projectRoot = createProjectRoot();
    const mdPath = path.join(projectRoot, 'src/prototypes/order-review/docs/prd-03-status.md');
    fs.writeFileSync(mdPath, '# PRD 03\n\n状态说明', 'utf8');
    const sourcePath = writeSource(projectRoot, {
      documentVersion: 1,
      format: 'axhub-annotation-source',
      data: { version: 2, prototypeName: 'order-review', pageId: 'order-review', nodes: [], updatedAt: 1 },
      markdownMap: {},
      assetMap: {},
      directory: {
        nodes: [
          {
            type: 'markdown',
            id: 'prd-03-status',
            title: 'PRD 03 | 状态',
            markdownPath: 'docs/prd-03-status.md',
          },
        ],
      },
    });

    const result = preprocessAnnotationSourceMarkdown({
      projectRoot,
      sourceFilePath: sourcePath,
      source: JSON.parse(fs.readFileSync(sourcePath, 'utf8')),
      mode: 'serve',
    });

    const node = result.source.directory.nodes[0];
    expect(node).toMatchObject({
      type: 'markdown',
      markdown: '# PRD 03\n\n状态说明',
      markdownPath: 'docs/prd-03-status.md',
    });
    expect(node).not.toHaveProperty('markdownEditUrl');
    expect(result.watchFiles).toEqual([mdPath]);
  });

  it('does not expose edit URLs when preprocessing for export builds', () => {
    const projectRoot = createProjectRoot();
    fs.writeFileSync(path.join(projectRoot, 'src/prototypes/order-review/docs/prd.md'), '# Exported PRD', 'utf8');
    const sourcePath = writeSource(projectRoot, {
      documentVersion: 1,
      format: 'axhub-annotation-source',
      data: { version: 2, prototypeName: 'order-review', pageId: 'order-review', nodes: [], updatedAt: 1 },
      markdownMap: {},
      assetMap: {},
      directory: {
        nodes: [{ type: 'markdown', id: 'prd', title: 'PRD', markdownPath: 'docs/prd.md' }],
      },
    });

    const result = preprocessAnnotationSourceMarkdown({
      projectRoot,
      sourceFilePath: sourcePath,
      source: JSON.parse(fs.readFileSync(sourcePath, 'utf8')),
      mode: 'build',
    });

    expect(result.source.directory.nodes[0]).toMatchObject({
      markdown: '# Exported PRD',
      markdownPath: 'docs/prd.md',
    });
    expect(result.source.directory.nodes[0]).not.toHaveProperty('markdownEditUrl');
  });

  it('recursively inlines markdownPath documents nested in directory folders', () => {
    const projectRoot = createProjectRoot();
    fs.writeFileSync(path.join(projectRoot, 'src/prototypes/order-review/docs/nested/prd.md'), '# Nested PRD', 'utf8');
    const sourcePath = writeSource(projectRoot, {
      documentVersion: 1,
      format: 'axhub-annotation-source',
      data: { version: 2, prototypeName: 'order-review', pageId: 'order-review', nodes: [], updatedAt: 1 },
      markdownMap: {},
      assetMap: {},
      directory: {
        nodes: [
          {
            type: 'folder',
            id: 'docs',
            title: '文档',
            children: [
              { type: 'markdown', id: 'nested-prd', title: 'Nested PRD', markdownPath: 'docs/nested/prd.md' },
            ],
          },
        ],
      },
    });

    const result = preprocessAnnotationSourceMarkdown({
      projectRoot,
      sourceFilePath: sourcePath,
      source: JSON.parse(fs.readFileSync(sourcePath, 'utf8')),
      mode: 'serve',
    });

    expect(result.source.directory.nodes[0].children[0]).toMatchObject({
      type: 'markdown',
      markdownPath: 'docs/nested/prd.md',
      markdown: '# Nested PRD',
    });
    expect(result.source.directory.nodes[0].children[0]).not.toHaveProperty('markdownEditUrl');
  });

  it.each([
    ['docs/prd.md', 'src/prototypes/order-review/docs/prd.md'],
    ['docs/nested/prd.md', 'src/prototypes/order-review/docs/nested/prd.md'],
  ])('allows safe prototype-relative markdownPath %s', (markdownPath, expectedProjectRelativePath) => {
    const projectRoot = createProjectRoot();
    const sourcePath = path.join(projectRoot, 'src/prototypes/order-review/annotation-source.json');

    const resolved = resolveAnnotationMarkdownPath(projectRoot, sourcePath, markdownPath);

    expect(resolved).toMatchObject({
      ok: true,
      projectRelativePath: expectedProjectRelativePath,
    });
  });

  it.each([
    '/abs.md',
    '../secret.md',
    'docs/../../secret.md',
    'docs//prd.md',
    'docs/%2e%2e/secret.md',
  ])('rejects unsafe markdownPath %s', (markdownPath) => {
    const projectRoot = createProjectRoot();
    const sourcePath = path.join(projectRoot, 'src/prototypes/order-review/annotation-source.json');

    const resolved = resolveAnnotationMarkdownPath(projectRoot, sourcePath, markdownPath);

    expect(resolved.ok).toBe(false);
  });

  it('falls back to existing inline markdown when the referenced file is missing and warns in dev', () => {
    const projectRoot = createProjectRoot();
    const sourcePath = writeSource(projectRoot, {
      documentVersion: 1,
      format: 'axhub-annotation-source',
      data: { version: 2, prototypeName: 'order-review', pageId: 'order-review', nodes: [], updatedAt: 1 },
      markdownMap: {},
      assetMap: {},
      directory: {
        nodes: [{ type: 'markdown', id: 'prd', title: 'PRD', markdownPath: 'docs/missing.md', markdown: '旧内容' }],
      },
    });
    const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined);

    const result = preprocessAnnotationSourceMarkdown({
      projectRoot,
      sourceFilePath: sourcePath,
      source: JSON.parse(fs.readFileSync(sourcePath, 'utf8')),
      mode: 'serve',
    });

    expect(result.source.directory.nodes[0]).toMatchObject({
      markdown: '旧内容',
      markdownPath: 'docs/missing.md',
    });
    expect(result.source.directory.nodes[0]).not.toHaveProperty('markdownEditUrl');
    expect(warn).toHaveBeenCalledWith(expect.stringContaining('docs/missing.md'));
  });

  it('loads annotation-source.json as an inlined JavaScript module in Vite', async () => {
    const projectRoot = createProjectRoot();
    fs.writeFileSync(path.join(projectRoot, 'src/prototypes/order-review/docs/prd.md'), '# Vite PRD', 'utf8');
    const sourcePath = writeSource(projectRoot, {
      documentVersion: 1,
      format: 'axhub-annotation-source',
      data: { version: 2, prototypeName: 'order-review', pageId: 'order-review', nodes: [], updatedAt: 1 },
      markdownMap: {},
      assetMap: {},
      directory: {
        nodes: [{ type: 'markdown', id: 'prd', title: 'PRD', markdownPath: 'docs/prd.md' }],
      },
    });
    const watched: string[] = [];
    const plugin = createAnnotationSourceMarkdownPlugin(projectRoot, { mode: 'serve' });

    const transform = plugin.transform as (
      this: { addWatchFile: (file: string) => void },
      code: string,
      id: string,
    ) => string | Promise<string> | null;
    const code = await transform.call(
      { addWatchFile: (file: string) => watched.push(file) },
      fs.readFileSync(sourcePath, 'utf8'),
      sourcePath,
    );

    expect(() => JSON.parse(String(code))).not.toThrow();
    expect(code).not.toContain('export default');
    expect(code).toContain('# Vite PRD');
    expect(code).not.toContain('markdownEditUrl');
    expect(watched).toEqual([path.join(projectRoot, 'src/prototypes/order-review/docs/prd.md')]);
  });
});
