import { flushPromises, mount } from '@vue/test-utils';

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

import SkillResourceWorkbench from './SkillResourceWorkbench.vue';
import workbenchSource from './SkillResourceWorkbench.vue?raw';

const apiMocks = vi.hoisted(() => ({
  createSkillFile: vi.fn(),
  deleteSkillFile: vi.fn(),
  downloadSkillFile: vi.fn(),
  getSkillFileContent: vi.fn(),
  getSkillFileTree: vi.fn(),
  previewSkillFile: vi.fn(),
  renameSkillFile: vi.fn(),
  saveSkillFile: vi.fn(),
  uploadSkillFile: vi.fn(),
  validateSkillForPublish: vi.fn(),
}));

vi.mock('./api', () => apiMocks);

const resources = [
  {
    isText: false,
    key: 'examples/archive.bin',
    mediaType: 'application/octet-stream',
    name: 'archive.bin',
    path: 'examples/archive.bin',
    size: 2048,
    type: 'EXAMPLE',
  },
  {
    isText: false,
    key: 'resources/diagram.png',
    mediaType: 'image/png',
    name: 'diagram.png',
    path: 'resources/diagram.png',
    size: 4096,
    type: 'OTHER',
  },
  {
    isText: false,
    key: 'references/manual.pdf',
    mediaType: 'application/pdf',
    name: 'manual.pdf',
    path: 'references/manual.pdf',
    size: 8192,
    type: 'REFERENCE',
  },
  {
    content: '{"ok":true}\n',
    isText: true,
    key: 'references/data.json',
    language: undefined,
    mediaType: 'application/json',
    name: 'data.json',
    path: 'references/data.json',
    size: 12,
    type: 'REFERENCE',
  },
  {
    content: 'const value: string = "ok";\n',
    isText: true,
    key: 'scripts/check.ts',
    language: 'TYPESCRIPT',
    mediaType: 'application/octet-stream',
    name: 'check.ts',
    path: 'scripts/check.ts',
    size: 28,
    type: 'SCRIPT',
  },
];

let wrapper: ReturnType<typeof mount> | undefined;

async function mountAndSelect(path: string, readonly = false) {
  wrapper = mount(SkillResourceWorkbench, {
    global: {
      directives: { loading: {} },
      stubs: {
        CodeEditor: {
          props: ['language'],
          template:
            '<div data-testid="code-editor" :data-language="language"></div>',
        },
        CodeViewer: {
          props: ['filename', 'language'],
          template:
            '<div data-testid="code-viewer" :data-filename="filename" :data-language="language"><button>复制</button><button>下载</button></div>',
        },
        MarkdownLiveEditor: {
          template: '<div data-testid="markdown-live-editor"></div>',
        },
        MarkdownSourceEditor: {
          template: '<div data-testid="markdown-source-editor"></div>',
        },
      },
    },
    props: { readonly, skillId: 101 },
  });
  await flushPromises();
  await wrapper.get(`[data-tree-path="${path}"]`).trigger('click');
  await flushPromises();
  return wrapper;
}

function toolbarButton(label: string) {
  return wrapper?.find(`button[aria-label="${label}"]`);
}

beforeEach(() => {
  vi.clearAllMocks();
  Object.defineProperty(URL, 'createObjectURL', {
    configurable: true,
    value: vi.fn(() => 'blob:skill-preview'),
  });
  Object.defineProperty(URL, 'revokeObjectURL', {
    configurable: true,
    value: vi.fn(),
  });
  apiMocks.getSkillFileTree.mockResolvedValue({
    data: [
      { key: 'SKILL.md', name: 'SKILL.md', path: 'SKILL.md', type: 'SKILL' },
      ...resources,
    ],
    errorCode: 0,
  });
  apiMocks.getSkillFileContent.mockImplementation(
    (_skillId: number, path: string) => {
      if (path === 'SKILL.md') {
        return Promise.resolve({ data: undefined, errorCode: 1 });
      }
      const file = resources.find((item) => item.path === path);
      return Promise.resolve({
        data: { ...file, content: '', contentHash: 'a'.repeat(64) },
        errorCode: file ? 0 : 1,
      });
    },
  );
});

afterEach(() => {
  wrapper?.unmount();
  wrapper = undefined;
  vi.useRealTimers();
});

describe('skill resource workbench binary resources', () => {
  it('routes a JSON reference to the code editor instead of Markdown live mode', async () => {
    await mountAndSelect('references/data.json');

    expect(wrapper?.find('[data-testid="code-editor"]').exists()).toBe(true);
    expect(
      wrapper?.find('[data-testid="code-editor"]').attributes('data-language'),
    ).toBe('json');
    expect(wrapper?.find('[data-testid="markdown-live-editor"]').exists()).toBe(
      false,
    );
  });

  it('uses the shared TypeScript highlighter for script resources', async () => {
    await mountAndSelect('scripts/check.ts');

    expect(
      wrapper?.find('[data-testid="code-editor"]').attributes('data-language'),
    ).toBe('typescript');
  });

  it('gives readonly generic text the shared copy and download viewer', async () => {
    await mountAndSelect('references/data.json', true);

    expect(wrapper?.find('[data-testid="code-editor"]').exists()).toBe(false);
    const viewer = wrapper?.get('[data-testid="code-viewer"]');
    expect(viewer?.attributes('data-filename')).toBe('references/data.json');
    expect(viewer?.attributes('data-language')).toBe('json');
    expect(viewer?.text()).toContain('复制');
    expect(viewer?.text()).toContain('下载');
  });

  it('renders generic EXAMPLE binaries with metadata and only meaningful actions', async () => {
    await mountAndSelect('examples/archive.bin');

    expect(wrapper?.text()).toContain('application/octet-stream');
    expect(wrapper?.text()).toContain('2.0 KB');
    expect(wrapper?.text()).toContain('此格式不支持在线预览');
    expect(toolbarButton('搜索')?.exists()).toBe(false);
    expect(toolbarButton('切换换行')?.exists()).toBe(false);
    expect(
      wrapper?.findAll('button').some((button) => button.text() === '保存'),
    ).toBe(false);
    expect(toolbarButton('进入全屏')?.exists()).toBe(true);
    expect(toolbarButton('重命名文件')?.exists()).toBe(false);
    expect(toolbarButton('删除文件')?.exists()).toBe(false);
    expect(
      wrapper
        ?.get('[data-tree-path="examples/archive.bin"]')
        .element.parentElement?.querySelector('[aria-label="更多操作"]'),
    ).not.toBeNull();
    expect(toolbarButton('下载资源')?.exists()).toBe(true);
    expect(
      wrapper?.find('.skill-resource-workbench__asset button').text(),
    ).toBe('下载');
    expect(apiMocks.previewSkillFile).not.toHaveBeenCalled();
  });

  it('previews an OTHER image only after the response MIME type is verified', async () => {
    apiMocks.previewSkillFile.mockResolvedValue(
      new Blob(['image'], { type: 'image/png' }),
    );

    await mountAndSelect('resources/diagram.png');

    expect(apiMocks.previewSkillFile).toHaveBeenCalledWith(
      101,
      'resources/diagram.png',
    );
    expect(URL.createObjectURL).toHaveBeenCalledTimes(1);
    expect(
      wrapper?.find('.skill-resource-workbench__asset-preview').exists(),
    ).toBe(true);
    expect(
      wrapper?.find('.skill-resource-workbench__asset-image').exists(),
    ).toBe(true);
  });

  it('renders a sandboxed PDF preview for any binary resource kind', async () => {
    apiMocks.previewSkillFile.mockResolvedValue(
      new Blob(['pdf'], { type: 'application/pdf' }),
    );

    await mountAndSelect('references/manual.pdf');

    const iframe = wrapper?.get('iframe');
    expect(iframe?.attributes('src')).toBe('blob:skill-preview');
    expect(iframe?.attributes('sandbox')).toBe('');
    expect(iframe?.attributes('referrerpolicy')).toBe('no-referrer');
    expect(iframe?.attributes('title')).toContain('PDF 预览');
  });

  it('rejects a preview response whose MIME type does not match the safe renderer', async () => {
    apiMocks.previewSkillFile.mockResolvedValue(
      new Blob(['<html></html>'], { type: 'text/html' }),
    );

    await mountAndSelect('resources/diagram.png');

    expect(URL.createObjectURL).not.toHaveBeenCalled();
    expect(
      wrapper?.find('.skill-resource-workbench__asset-image').exists(),
    ).toBe(false);
    expect(wrapper?.text()).toContain('预览加载失败');
    expect(wrapper?.text()).toContain('可下载后使用本地应用打开');
  });

  it('falls back to download when an image renderer rejects valid-MIME content', async () => {
    apiMocks.previewSkillFile.mockResolvedValue(
      new Blob(['broken'], { type: 'image/png' }),
    );
    await mountAndSelect('resources/diagram.png');

    await wrapper
      ?.get('.skill-resource-workbench__asset-image')
      .trigger('error');
    await flushPromises();

    expect(
      wrapper?.find('.skill-resource-workbench__asset-image').exists(),
    ).toBe(false);
    expect(wrapper?.text()).toContain('预览加载失败');
    expect(wrapper?.text()).toContain('可下载后使用本地应用打开');
  });

  it('falls back to download when the PDF iframe reports a render error', async () => {
    apiMocks.previewSkillFile.mockResolvedValue(
      new Blob(['broken'], { type: 'application/pdf' }),
    );
    await mountAndSelect('references/manual.pdf');

    await wrapper?.get('iframe').trigger('error');
    await flushPromises();

    expect(wrapper?.find('iframe').exists()).toBe(false);
    expect(wrapper?.text()).toContain('预览加载失败');
  });

  it('falls back when the PDF renderer never reports readiness', async () => {
    vi.useFakeTimers();
    apiMocks.previewSkillFile.mockResolvedValue(
      new Blob(['pdf'], { type: 'application/pdf' }),
    );
    await mountAndSelect('references/manual.pdf');

    vi.advanceTimersByTime(8001);
    await flushPromises();

    expect(wrapper?.find('iframe').exists()).toBe(false);
    expect(wrapper?.text()).toContain('预览加载失败');
  });
});

describe('skill resource workbench validation', () => {
  it('uses the publish preflight endpoint for publish validation', async () => {
    apiMocks.validateSkillForPublish.mockResolvedValue({
      data: { issues: [], valid: true },
      errorCode: 0,
    });
    wrapper = mount(SkillResourceWorkbench, {
      global: {
        directives: { loading: {} },
        stubs: {
          MarkdownLiveEditor: {
            template: '<div data-testid="markdown-live-editor"></div>',
          },
        },
      },
      props: { skillId: 101 },
    });
    await flushPromises();

    const valid = await (
      wrapper.vm as unknown as {
        validateAll: () => Promise<boolean>;
      }
    ).validateAll();

    expect(valid).toBe(true);
    expect(apiMocks.validateSkillForPublish).toHaveBeenCalledWith(101);
    expect(wrapper.find('.document-editor-workbench__inspector').exists()).toBe(
      false,
    );
  });

  it('keeps validation issues collapsed until the user opens them', async () => {
    apiMocks.validateSkillForPublish.mockResolvedValue({
      data: {
        issues: [
          {
            line: 3,
            message: '缺少必填字段',
            path: 'SKILL.md',
            severity: 'ERROR',
          },
        ],
        valid: false,
      },
      errorCode: 0,
    });
    wrapper = mount(SkillResourceWorkbench, {
      global: {
        directives: { loading: {} },
        stubs: {
          MarkdownLiveEditor: {
            template: '<div data-testid="markdown-live-editor"></div>',
          },
        },
      },
      props: { skillId: 101 },
    });
    await flushPromises();

    const valid = await (
      wrapper.vm as unknown as {
        validateAll: () => Promise<boolean>;
      }
    ).validateAll();
    await flushPromises();

    expect(valid).toBe(false);
    expect(wrapper.find('.document-editor-workbench__inspector').exists()).toBe(
      false,
    );
    const issueButton = wrapper.get('button[aria-label="查看校验问题"]');
    expect(issueButton.text()).toContain('校验问题 1');

    await issueButton.trigger('click');

    expect(wrapper.find('.document-editor-workbench__inspector').exists()).toBe(
      true,
    );
  });
});

describe('skill resource workbench navigation', () => {
  it('keeps the current file open when a directory is expanded', async () => {
    await mountAndSelect('references/data.json');

    await wrapper?.get('[data-tree-path="references"]').trigger('click');
    await flushPromises();

    expect(wrapper?.find('[data-tree-path="scripts"]').exists()).toBe(true);
    expect(wrapper?.find('[data-tree-path="assets"]').exists()).toBe(true);
    expect(wrapper?.text()).toContain('references/data.json');
    expect(wrapper?.find('[data-testid="code-editor"]').exists()).toBe(true);
    expect(wrapper?.text()).not.toContain('此目录暂无内容');

    const referenceRow = wrapper?.get('[data-tree-path="references"]').element
      .parentElement;
    referenceRow
      ?.querySelector<HTMLButtonElement>('[aria-label="在此目录新建文件"]')
      ?.click();
    await flushPromises();

    expect(
      wrapper
        ?.findAllComponents({ name: 'ElSelect' })
        .some((select) => select.props('modelValue') === 'references'),
    ).toBe(true);
    const fileNameInput = wrapper
      ?.findAllComponents({ name: 'ElInput' })
      .find((input) => input.props('placeholder') === '例如 guide.md');
    expect(fileNameInput?.props('modelValue')).toBe('');
  });

  it('keeps resource navigation focused on the standard package tree', async () => {
    await mountAndSelect('references');

    expect(wrapper?.text()).not.toContain('能力绑定');
    expect(workbenchSource).not.toContain('instructionsOnly');
    expect(workbenchSource).not.toContain('selectSkillFile');
    expect(workbenchSource).toContain('allow-create');
    expect(workbenchSource).toContain('placeholder="选择或输入目录"');
  });
});
