/**
 * TDD Tests for FEATURE-038-C: Enhanced Deliverable Viewer (Frontend)
 * Tests: Folder detection, file-tree rendering, inline preview
 *
 * TDD: All tests MUST fail until deliverable-viewer.js is implemented.
 */
import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from 'vitest';
import { loadFeatureScript } from './helpers.js';

/**
 * Guard: loads the feature script once.
 */
let _implLoaded = false;
function ensureImpl() {
  if (!_implLoaded) {
    try {
      // CR-008: Load FilePreviewRenderer dependency before deliverable-viewer
      loadFeatureScript('../core/file-preview-renderer.js');
      loadFeatureScript('deliverable-viewer.js');
      _implLoaded = true;
    } catch {
      // File not yet implemented — TDD
    }
  }
  return typeof globalThis.DeliverableViewer !== 'undefined';
}

describe('FEATURE-038-C: Enhanced Deliverable Viewer', () => {
  beforeEach(() => {
    document.body.innerHTML = '';
    globalThis.fetch = vi.fn();
    globalThis.marked = { parse: vi.fn((md) => `<p>${md}</p>`) };
    ensureImpl();
  });

  afterEach(() => {
    document.body.innerHTML = '';
  });

  describe('Folder Detection', () => {
    it('should export DeliverableViewer class', () => {
      expect(globalThis.DeliverableViewer).toBeDefined();
    });

    it('should identify paths ending with / as folder-type', () => {
      const DV = globalThis.DeliverableViewer;
      expect(DV).toBeDefined();
      expect(DV.isFolderType('x-ipe-docs/ideas/025/refined-idea/')).toBe(true);
    });

    it('should identify paths without extension as folder-type', () => {
      const DV = globalThis.DeliverableViewer;
      expect(DV).toBeDefined();
      expect(DV.isFolderType('x-ipe-docs/ideas/wf-001-test-the-workflow')).toBe(true);
    });

    it('should identify paths with extension as file-type', () => {
      const DV = globalThis.DeliverableViewer;
      expect(DV).toBeDefined();
      expect(DV.isFolderType('x-ipe-docs/ideas/025/idea-summary.md')).toBe(false);
    });

    it('should identify numbered folder names with dots as folder-type', () => {
      const DV = globalThis.DeliverableViewer;
      expect(DV).toBeDefined();
      // Folder names like "001. Feature Name" contain dots but are not file extensions
      expect(DV.isFolderType('ideas/001. Feature-Console Voice Input - 01242026 000728')).toBe(true);
      expect(DV.isFolderType('x-ipe-docs/ideas/002. Feature-Brand Themes')).toBe(true);
      // Files should still be detected correctly
      expect(DV.isFolderType('ideas/001. Feature-Console Voice Input/new idea.md')).toBe(false);
      expect(DV.isFolderType('ideas/002. Feature-Brand Themes/mockup.html')).toBe(false);
    });
  });

  describe('File-Tree Rendering', () => {
    it('FEATURE-039-A: folder card no longer has expand toggle (modal replaces inline tree)', () => {
      const DV = globalThis.DeliverableViewer;
      expect(DV).toBeDefined();
      const viewer = new DV({ workflowName: 'hello' });
      const card = viewer.renderFolderDeliverable({ path: 'refined-idea/', name: 'refined-idea' });
      expect(card.querySelector('.toggle-icon')).toBeNull();
      expect(card.classList.contains('clickable')).toBe(true);
    });

    it('FEATURE-039-A: folder card no longer has inline tree container', () => {
      const DV = globalThis.DeliverableViewer;
      expect(DV).toBeDefined();
      const viewer = new DV({ workflowName: 'hello' });
      const card = viewer.renderFolderDeliverable({ path: 'refined-idea/', name: 'refined-idea' });
      expect(card.querySelector('.deliverable-tree')).toBeNull();
    });

    it('should render nested ul/li structure for tree', () => {
      const DV = globalThis.DeliverableViewer;
      expect(DV).toBeDefined();
      const entries = [
        { name: 'file.md', type: 'file', path: 'folder/file.md' },
        { name: 'sub', type: 'dir', path: 'folder/sub/', children: [] },
      ];
      const tree = DV.buildTreeDOM(entries);
      expect(tree.tagName).toBe('UL');
      expect(tree.children.length).toBe(2);
    });

    it('should show folder icons for directories', () => {
      const DV = globalThis.DeliverableViewer;
      expect(DV).toBeDefined();
      const entries = [{ name: 'sub', type: 'dir', path: 'folder/sub/', children: [] }];
      const tree = DV.buildTreeDOM(entries);
      expect(tree.querySelector('.dir-icon')).not.toBeNull();
    });

    it('should show file icons for files', () => {
      const DV = globalThis.DeliverableViewer;
      expect(DV).toBeDefined();
      const entries = [{ name: 'readme.md', type: 'file', path: 'folder/readme.md' }];
      const tree = DV.buildTreeDOM(entries);
      expect(tree.querySelector('.file-icon')).not.toBeNull();
    });

    it('should make subdirectories collapsible', () => {
      const DV = globalThis.DeliverableViewer;
      expect(DV).toBeDefined();
      const entries = [
        { name: 'sub', type: 'dir', path: 'folder/sub/', children: [
          { name: 'inner.md', type: 'file', path: 'folder/sub/inner.md' }
        ]},
      ];
      const tree = DV.buildTreeDOM(entries);
      const dirItem = tree.querySelector('.dir-item');
      expect(dirItem).not.toBeNull();
      dirItem.click();
      expect(dirItem.querySelector('ul').style.display).toBe('none');
    });

    it('should show "No files" for empty folders', () => {
      const DV = globalThis.DeliverableViewer;
      expect(DV).toBeDefined();
      const tree = DV.buildTreeDOM([]);
      expect(tree.textContent).toContain('No files');
    });
  });

  describe('Inline Preview', () => {
    it('should open preview pane when file clicked', async () => {
      globalThis.fetch.mockResolvedValue({
        ok: true,
        text: async () => '# Test Heading\n\nSome content',
        headers: { get: () => null },
      });

      const DV = globalThis.DeliverableViewer;
      expect(DV).toBeDefined();
      const viewer = new DV({ workflowName: 'hello' });
      await viewer.showPreview('refined-idea/readme.md');
      expect(document.querySelector('.deliverable-preview')).not.toBeNull();
    });

    it('should render markdown files as formatted content', async () => {
      globalThis.fetch.mockResolvedValue({
        ok: true,
        text: async () => '# Heading',
        headers: { get: () => null },
      });

      const DV = globalThis.DeliverableViewer;
      expect(DV).toBeDefined();
      const viewer = new DV({ workflowName: 'hello' });
      await viewer.showPreview('refined-idea/readme.md');
      // CR-008: Rendering delegated to FilePreviewRenderer which uses ContentRenderer
      const content = document.querySelector('.preview-content');
      expect(content).not.toBeNull();
      expect(content.innerHTML).not.toBe('');
    });

    it('should render text files as preformatted text', async () => {
      globalThis.fetch.mockResolvedValue({
        ok: true,
        text: async () => 'plain text content',
        headers: { get: () => null },
      });

      const DV = globalThis.DeliverableViewer;
      expect(DV).toBeDefined();
      const viewer = new DV({ workflowName: 'hello' });
      await viewer.showPreview('refined-idea/data.txt');
      expect(document.querySelector('.deliverable-preview pre')).not.toBeNull();
    });

    it('should show file name in preview header', async () => {
      globalThis.fetch.mockResolvedValue({
        ok: true,
        text: async () => 'content',
        headers: { get: () => null },
      });

      const DV = globalThis.DeliverableViewer;
      expect(DV).toBeDefined();
      const viewer = new DV({ workflowName: 'hello' });
      await viewer.showPreview('refined-idea/readme.md');
      expect(document.querySelector('.preview-header').textContent).toContain('readme.md');
    });

    it('should replace previous preview when new file clicked', async () => {
      globalThis.fetch.mockResolvedValue({
        ok: true,
        text: async () => 'content',
        headers: { get: () => null },
      });

      const DV = globalThis.DeliverableViewer;
      expect(DV).toBeDefined();
      const viewer = new DV({ workflowName: 'hello' });
      await viewer.showPreview('file1.md');
      await viewer.showPreview('file2.md');
      const previews = document.querySelectorAll('.deliverable-preview');
      expect(previews.length).toBe(1);
    });

    it('should show message for binary files (415 response)', async () => {
      globalThis.fetch.mockResolvedValue({
        ok: false,
        status: 415,
      });

      const DV = globalThis.DeliverableViewer;
      expect(DV).toBeDefined();
      const viewer = new DV({ workflowName: 'hello' });
      // Use a non-convertible binary file (e.g. .zip) to test the 415 fallback
      await viewer.showPreview('archive.zip');
      expect(document.querySelector('.deliverable-preview').textContent).toContain('Binary');
    });

    it('should render image files with an img tag instead of fetching as text', async () => {
      const DV = globalThis.DeliverableViewer;
      expect(DV).toBeDefined();
      const viewer = new DV({ workflowName: 'hello' });
      await viewer.showPreview('photo.png');
      const img = document.querySelector('.deliverable-preview img');
      expect(img).not.toBeNull();
      expect(img.src).toContain('api/ideas/file');
      expect(img.src).toContain('photo.png');
    });

    it('should render PDF files with an iframe', async () => {
      const DV = globalThis.DeliverableViewer;
      expect(DV).toBeDefined();
      const viewer = new DV({ workflowName: 'hello' });
      await viewer.showPreview('report.pdf');
      const iframe = document.querySelector('.deliverable-preview iframe');
      expect(iframe).not.toBeNull();
      expect(iframe.src).toContain('api/ideas/file');
      expect(iframe.src).toContain('report.pdf');
    });
  });

  describe('TASK-588: Deliverable Card Styling Fixes', () => {
    it('should render folder icon with deliverable-icon class and background', () => {
      const DV = globalThis.DeliverableViewer;
      expect(DV).toBeDefined();
      const viewer = new DV({ workflowName: 'hello' });
      const card = viewer.renderFolderDeliverable({ path: 'ideas/wf-001-greedy-snake', name: 'wf-001-greedy-snake', category: 'ideas' });
      const icon = card.querySelector('.deliverable-icon');
      expect(icon).not.toBeNull();
      expect(icon.textContent).toBe('📁');
    });

    it('should render folder name and path on separate lines', () => {
      const DV = globalThis.DeliverableViewer;
      expect(DV).toBeDefined();
      const viewer = new DV({ workflowName: 'hello' });
      const card = viewer.renderFolderDeliverable({ path: 'ideas/wf-001-greedy-snake', name: 'wf-001-greedy-snake', category: 'ideas' });
      const info = card.querySelector('.deliverable-info');
      expect(info).not.toBeNull();
      const name = info.querySelector('.deliverable-name');
      const path = info.querySelector('.deliverable-path');
      expect(name).not.toBeNull();
      expect(path).not.toBeNull();
      // name and path should be block elements (div), not inline (span)
      expect(name.tagName).toBe('DIV');
      expect(path.tagName).toBe('DIV');
    });

    it('should strip x-ipe-docs/ prefix from folder deliverable path', () => {
      const DV = globalThis.DeliverableViewer;
      expect(DV).toBeDefined();
      const viewer = new DV({ workflowName: 'hello' });
      const card = viewer.renderFolderDeliverable({ path: 'x-ipe-docs/ideas/wf-001-greedy-snake', name: 'wf-001-greedy-snake', category: 'ideas' });
      const path = card.querySelector('.deliverable-path');
      expect(path.textContent).not.toContain('x-ipe-docs/');
      expect(path.textContent).toBe('ideas/wf-001-greedy-snake');
    });
  });

  describe('TASK-589: File Deliverable Click-to-Preview', () => {
    it('should have makeClickableForPreview method', () => {
      const DV = globalThis.DeliverableViewer;
      expect(DV).toBeDefined();
      const viewer = new DV({ workflowName: 'hello' });
      expect(typeof viewer.makeClickableForPreview).toBe('function');
    });

    it('should set cursor pointer on card', () => {
      const DV = globalThis.DeliverableViewer;
      expect(DV).toBeDefined();
      const viewer = new DV({ workflowName: 'hello' });
      const card = document.createElement('div');
      viewer.makeClickableForPreview(card, 'ideas/test/idea-summary.md');
      expect(card.style.cursor).toBe('pointer');
    });

    it('should call showPreview on click', async () => {
      globalThis.fetch.mockResolvedValue({
        ok: true,
        text: async () => '# Content',
        headers: { get: () => null },
      });

      const DV = globalThis.DeliverableViewer;
      expect(DV).toBeDefined();
      const viewer = new DV({ workflowName: 'hello' });
      vi.spyOn(viewer, 'showPreview');
      const card = document.createElement('div');
      viewer.makeClickableForPreview(card, 'ideas/test/idea-summary.md');
      document.body.appendChild(card);
      card.click();
      expect(viewer.showPreview).toHaveBeenCalledWith('ideas/test/idea-summary.md');
    });

    it('should not make missing deliverables clickable', () => {
      const DV = globalThis.DeliverableViewer;
      expect(DV).toBeDefined();
      const viewer = new DV({ workflowName: 'hello' });
      const card = document.createElement('div');
      card.classList.add('missing');
      viewer.makeClickableForPreview(card, 'ideas/test/idea-summary.md', { exists: false });
      expect(card.style.cursor).not.toBe('pointer');
    });
  });

  describe('TASK-615: HTML Preview as Iframe', () => {
    it('should render HTML files in an iframe instead of raw text', async () => {
      globalThis.URL.createObjectURL = vi.fn(() => 'blob:mock-html');
      globalThis.URL.revokeObjectURL = vi.fn();
      globalThis.fetch.mockResolvedValue({
        ok: true,
        text: async () => '<html><body><h1>Hello</h1></body></html>',
        headers: { get: () => null },
      });

      const DV = globalThis.DeliverableViewer;
      expect(DV).toBeDefined();
      const viewer = new DV({ workflowName: 'hello' });
      await viewer.showPreview('mockups/design.html');
      const iframe = document.querySelector('.preview-content iframe');
      expect(iframe).not.toBeNull();
      expect(iframe.getAttribute('sandbox')).toContain('allow-scripts');
    });

    it('should render .htm files in an iframe', async () => {
      globalThis.URL.createObjectURL = vi.fn(() => 'blob:mock-htm');
      globalThis.URL.revokeObjectURL = vi.fn();
      globalThis.fetch.mockResolvedValue({
        ok: true,
        text: async () => '<html><body>Test</body></html>',
        headers: { get: () => null },
      });

      const DV = globalThis.DeliverableViewer;
      expect(DV).toBeDefined();
      const viewer = new DV({ workflowName: 'hello' });
      await viewer.showPreview('mockups/page.htm');
      const iframe = document.querySelector('.preview-content iframe');
      expect(iframe).not.toBeNull();
    });

    it('should NOT show HTML files as preformatted text', async () => {
      globalThis.URL.createObjectURL = vi.fn(() => 'blob:mock-no-pre');
      globalThis.URL.revokeObjectURL = vi.fn();
      globalThis.fetch.mockResolvedValue({
        ok: true,
        text: async () => '<html><body><h1>Hello</h1></body></html>',
        headers: { get: () => null },
      });

      const DV = globalThis.DeliverableViewer;
      expect(DV).toBeDefined();
      const viewer = new DV({ workflowName: 'hello' });
      await viewer.showPreview('mockups/design.html');
      const pre = document.querySelector('.preview-content pre');
      expect(pre).toBeNull();
    });
  });

  describe('TASK-680: Markdown preview uses ContentRenderer (same as folder browser)', () => {
    it('should use ContentRenderer for markdown files when available', async () => {
      // CR-008: FilePreviewRenderer delegates to ContentRenderer internally.
      // We verify that markdown renders (not as raw text or error).
      globalThis.fetch.mockResolvedValue({
        ok: true,
        text: async () => '# Heading with mermaid',
        headers: { get: () => null },
      });

      const DV = globalThis.DeliverableViewer;
      expect(DV).toBeDefined();
      const viewer = new DV({ workflowName: 'hello' });
      await viewer.showPreview('refined-idea/readme.md');

      const content = document.querySelector('.preview-content');
      expect(content).not.toBeNull();
      // ContentRenderer should have rendered the markdown (not raw text)
      expect(content.querySelector('pre')?.textContent).not.toBe('# Heading with mermaid');
    });

    it('should render markdown content without errors', async () => {
      globalThis.fetch.mockResolvedValue({
        ok: true,
        text: async () => '# Fallback',
        headers: { get: () => null },
      });

      const DV = globalThis.DeliverableViewer;
      expect(DV).toBeDefined();
      const viewer = new DV({ workflowName: 'hello' });
      await viewer.showPreview('refined-idea/readme.md');

      const content = document.querySelector('.preview-content');
      expect(content).not.toBeNull();
      expect(content.innerHTML).not.toBe('');
      expect(content.textContent).not.toContain('Failed');
    });
  });

  describe('Security', () => {
    it('should not allow path traversal in file requests', async () => {
      const DV = globalThis.DeliverableViewer;
      expect(DV).toBeDefined();
      const viewer = new DV({ workflowName: 'hello' });
      await expect(viewer.showPreview('../../etc/passwd')).rejects.toThrow();
    });
  });

  /* --------------------------------------------------------------------------
     TASK-875: Deliverable labels show file/folder names instead of tag names
     -------------------------------------------------------------------------- */
  /* --------------------------------------------------------------------------
     TASK-876: Folder chips rendered inline beside section subtitle
     -------------------------------------------------------------------------- */
  describe('TASK-876: renderFolderChip creates small inline chip', () => {
    it('should have renderFolderChip method', () => {
      const DV = globalThis.DeliverableViewer;
      expect(DV).toBeDefined();
      const viewer = new DV({ workflowName: 'hello' });
      expect(typeof viewer.renderFolderChip).toBe('function');
    });

    it('should render a chip with deliverable-folder-chip class', () => {
      const DV = globalThis.DeliverableViewer;
      const viewer = new DV({ workflowName: 'hello' });
      const chip = viewer.renderFolderChip({ path: 'x-ipe-docs/ideas/wf-001-test', name: 'ideas-folder', category: 'ideas' });
      expect(chip.classList.contains('deliverable-folder-chip')).toBe(true);
    });

    it('should show folder name from path basename', () => {
      const DV = globalThis.DeliverableViewer;
      const viewer = new DV({ workflowName: 'hello' });
      const chip = viewer.renderFolderChip({ path: 'x-ipe-docs/ideas/wf-001-test', name: 'ideas-folder', category: 'ideas' });
      expect(chip.textContent).toContain('wf-001-test');
    });

    it('should preserve trailing slash when folder path ends with slash', () => {
      const DV = globalThis.DeliverableViewer;
      const viewer = new DV({ workflowName: 'hello' });
      const chip = viewer.renderFolderChip({
        path: 'x-ipe-docs/requirements/EPIC-050/FEATURE-050-B/',
        name: 'feature-docs-folder',
        category: 'requirements'
      });
      expect(chip.textContent).toContain('FEATURE-050-B/');
      expect(chip.textContent).not.toContain('feature-docs-folder');
    });

    it('should include a folder icon', () => {
      const DV = globalThis.DeliverableViewer;
      const viewer = new DV({ workflowName: 'hello' });
      const chip = viewer.renderFolderChip({ path: 'ideas/my-folder/', name: 'my-folder', category: 'ideas' });
      expect(chip.textContent).toContain('📁');
    });

    it('should be clickable', () => {
      const DV = globalThis.DeliverableViewer;
      const viewer = new DV({ workflowName: 'hello' });
      const chip = viewer.renderFolderChip({ path: 'ideas/my-folder', name: 'my-folder', category: 'ideas' });
      expect(chip.classList.contains('clickable')).toBe(true);
      expect(chip.style.cursor).toBe('pointer');
    });

    it('should fall back to name when path is empty', () => {
      const DV = globalThis.DeliverableViewer;
      const viewer = new DV({ workflowName: 'hello' });
      const chip = viewer.renderFolderChip({ path: '', name: 'fallback-name', category: 'ideas' });
      expect(chip.textContent).toContain('fallback-name');
    });

    it('should set title attribute to full path', () => {
      const DV = globalThis.DeliverableViewer;
      const viewer = new DV({ workflowName: 'hello' });
      const chip = viewer.renderFolderChip({ path: 'x-ipe-docs/ideas/wf-001-test', name: 'ideas', category: 'ideas' });
      expect(chip.title).toBe('x-ipe-docs/ideas/wf-001-test');
    });
  });

  describe('TASK-875: Deliverable card labels use file/folder names', () => {
    it('renderFolderDeliverable shows folder name, not tag name', () => {
      const DV = globalThis.DeliverableViewer;
      expect(DV).toBeDefined();
      const viewer = new DV({ workflowName: 'hello' });
      const card = viewer.renderFolderDeliverable({
        path: 'x-ipe-docs/ideas/my-cool-idea',
        name: 'ideas-folder',
        category: 'folders'
      });
      const name = card.querySelector('.deliverable-name');
      expect(name.textContent).toBe('my-cool-idea');
    });

    it('renderFolderDeliverable falls back to tag name when path is empty', () => {
      const DV = globalThis.DeliverableViewer;
      expect(DV).toBeDefined();
      const viewer = new DV({ workflowName: 'hello' });
      const card = viewer.renderFolderDeliverable({
        path: '',
        name: 'ideas-folder',
        category: 'folders'
      });
      const name = card.querySelector('.deliverable-name');
      expect(name.textContent).toBe('ideas-folder');
    });

    it('renderFolderDeliverable preserves trailing slash for folder paths', () => {
      const DV = globalThis.DeliverableViewer;
      expect(DV).toBeDefined();
      const viewer = new DV({ workflowName: 'hello' });
      const card = viewer.renderFolderDeliverable({
        path: 'x-ipe-docs/requirements/EPIC-050/FEATURE-050-B/',
        name: 'feature-docs-folder',
        category: 'folders'
      });
      const name = card.querySelector('.deliverable-name');
      expect(name.textContent).toBe('FEATURE-050-B/');
    });
  });
});

/* --------------------------------------------------------------------------
   TASK-875: File deliverable card labels (workflow-stage._renderDeliverableCard)
   -------------------------------------------------------------------------- */
describe('TASK-875: _renderDeliverableCard labels use file names', () => {
  beforeAll(() => {
    loadFeatureScript('workflow-stage.js');
  });

  it('shows file basename instead of tag name', () => {
    const card = workflowStage._renderDeliverableCard({
      name: 'raw-ideas',
      path: 'x-ipe-docs/ideas/my-idea/sketch1.png',
      category: 'ideas',
      exists: true
    });
    const name = card.querySelector('.deliverable-name');
    expect(name.textContent).toBe('sketch1.png');
  });

  it('shows file basename for nested paths', () => {
    const card = workflowStage._renderDeliverableCard({
      name: 'specification',
      path: 'x-ipe-docs/requirements/EPIC-041/FEATURE-041-E/specification.md',
      category: 'requirements',
      exists: true
    });
    const name = card.querySelector('.deliverable-name');
    expect(name.textContent).toBe('specification.md');
  });

  it('falls back to tag name when path is empty', () => {
    const card = workflowStage._renderDeliverableCard({
      name: 'raw-ideas',
      path: '',
      category: 'ideas',
      exists: false
    });
    const name = card.querySelector('.deliverable-name');
    expect(name.textContent).toBe('raw-ideas');
  });

  it('shows tag name for hidden/metadata files (dotfiles)', () => {
    const card = workflowStage._renderDeliverableCard({
      name: 'kb-references',
      path: 'x-ipe-docs/ideas/my-idea/.knowledge-reference.yaml',
      category: 'ideas',
      exists: true
    });
    const name = card.querySelector('.deliverable-name');
    expect(name.textContent).toBe('kb-references');
  });
});


// ============================================================================
// CR-001: .docx/.msg Preview Handling
// ============================================================================

describe('CR-001: Convertible Binary Preview (.docx / .msg)', () => {
  beforeEach(() => {
    document.body.innerHTML = '';
    globalThis.fetch = vi.fn();
    globalThis.marked = { parse: vi.fn((md) => `<p>${md}</p>`) };
    // JSDOM does not support URL.createObjectURL — mock it
    if (!globalThis.URL.createObjectURL) {
      globalThis.URL.createObjectURL = vi.fn(() => 'blob:mock-url');
    }
    if (!globalThis.URL.revokeObjectURL) {
      globalThis.URL.revokeObjectURL = vi.fn();
    }
    ensureImpl();
  });

  afterEach(() => {
    document.body.innerHTML = '';
  });

  it('AC-038-C.04a: renders X-Converted response in sandboxed iframe', async () => {
    const DV = globalThis.DeliverableViewer;
    if (!DV) return; // TDD guard

    globalThis.fetch.mockResolvedValue({
      ok: true,
      headers: { get: (k) => k === 'X-Converted' ? 'true' : null },
      text: async () => '<h1>Converted Doc</h1>',
    });

    const viewer = new DV({ workflowName: 'test-wf' });
    await viewer.showPreview('x-ipe-docs/ideas/test/report.docx');

    const iframe = document.querySelector('.deliverable-preview-backdrop iframe');
    expect(iframe).not.toBeNull();
    expect(iframe.getAttribute('sandbox')).toBe('allow-same-origin');
  });

  it('AC-038-C.04f: shows size message for 413 response', async () => {
    const DV = globalThis.DeliverableViewer;
    if (!DV) return;

    globalThis.fetch.mockResolvedValue({
      ok: false,
      status: 413,
      headers: { get: () => null },
    });

    const viewer = new DV({ workflowName: 'test-wf' });
    await viewer.showPreview('x-ipe-docs/ideas/test/huge.docx');

    const content = document.querySelector('.preview-content');
    expect(content).not.toBeNull();
    expect(content.textContent.toLowerCase()).toContain('too large');
  });

  it('AC-038-C.04h: 415 still shows binary file message (unchanged)', async () => {
    const DV = globalThis.DeliverableViewer;
    if (!DV) return;

    globalThis.fetch.mockResolvedValue({
      ok: false,
      status: 415,
      headers: { get: () => null },
    });

    const viewer = new DV({ workflowName: 'test-wf' });
    await viewer.showPreview('x-ipe-docs/ideas/test/archive.zip');

    const content = document.querySelector('.preview-content');
    expect(content).not.toBeNull();
    expect(content.textContent.toLowerCase()).toContain('binary file');
  });

  it('AC-038-C.04g: iframe has no allow-scripts for converted content', async () => {
    const DV = globalThis.DeliverableViewer;
    if (!DV) return;

    globalThis.fetch.mockResolvedValue({
      ok: true,
      headers: { get: (k) => k === 'X-Converted' ? 'true' : null },
      text: async () => '<p>Safe content</p>',
    });

    const viewer = new DV({ workflowName: 'test-wf' });
    await viewer.showPreview('x-ipe-docs/ideas/test/report.docx');

    const iframe = document.querySelector('.deliverable-preview-backdrop iframe');
    expect(iframe).not.toBeNull();
    const sandbox = iframe.getAttribute('sandbox');
    expect(sandbox).not.toContain('allow-scripts');
  });

  // CR-002: Preview header toolbar, download, toggle, smart defaults
  describe('CR-002: Preview header toolbar', () => {
    let DV;
    beforeEach(() => {
      DV = globalThis.DeliverableViewer;
      globalThis.URL.createObjectURL = vi.fn(() => 'blob:mock-url');
      globalThis.URL.revokeObjectURL = vi.fn();
    });

    it('shows download, toggle, and close in header for text-renderable file', async () => {
      globalThis.fetch = vi.fn().mockResolvedValue({
        ok: true,
        text: async () => '# Hello',
        headers: { get: () => null }
      });
      globalThis.marked = { parse: vi.fn((md) => `<p>${md}</p>`) };

      const viewer = new DV({ workflowName: 'test-wf' });
      await viewer.showPreview('x-ipe-docs/ideas/test/readme.md');

      const header = document.querySelector('.preview-header');
      expect(header).not.toBeNull();
      expect(header.querySelector('.preview-title')).not.toBeNull();
      expect(header.querySelector('.preview-download-btn')).not.toBeNull();
      expect(header.querySelector('.preview-toggle-btn')).not.toBeNull();
      expect(header.querySelector('.preview-close')).not.toBeNull();
    });

    it('hides toggle button for non-text-renderable file (image)', async () => {
      const viewer = new DV({ workflowName: 'test-wf' });
      await viewer.showPreview('x-ipe-docs/ideas/test/logo.png');

      const header = document.querySelector('.preview-header');
      expect(header.querySelector('.preview-download-btn')).not.toBeNull();
      expect(header.querySelector('.preview-toggle-btn')).toBeNull();
      expect(header.querySelector('.preview-close')).not.toBeNull();
    });

    it('download link has correct href with download=true', async () => {
      globalThis.fetch = vi.fn().mockResolvedValue({
        ok: true,
        text: async () => 'content',
        headers: { get: () => null }
      });
      globalThis.marked = { parse: vi.fn((md) => `<p>${md}</p>`) };

      const viewer = new DV({ workflowName: 'test-wf' });
      await viewer.showPreview('x-ipe-docs/ideas/test/readme.md');

      const downloadBtn = document.querySelector('.preview-download-btn');
      expect(downloadBtn.href).toContain('download=true');
      expect(downloadBtn.getAttribute('download')).toBe('readme.md');
    });
  });

  describe('CR-002: Smart defaults', () => {
    let DV;
    beforeEach(() => {
      DV = globalThis.DeliverableViewer;
      globalThis.URL.createObjectURL = vi.fn(() => 'blob:mock-url');
      globalThis.URL.revokeObjectURL = vi.fn();
    });

    it('defaults to raw mode for files under /src/ path', async () => {
      globalThis.fetch = vi.fn().mockResolvedValue({
        ok: true,
        text: async () => 'const x = 1;',
        headers: { get: () => null }
      });

      const viewer = new DV({ workflowName: 'test-wf' });
      await viewer.showPreview('x-ipe-docs/ideas/project/src/app.js');

      // Should render in raw mode — look for raw <pre>
      const pre = document.querySelector('.preview-raw-content');
      expect(pre).not.toBeNull();
      expect(pre.textContent).toBe('const x = 1;');
    });

    it('defaults to auto mode for non-src files', async () => {
      globalThis.fetch = vi.fn().mockResolvedValue({
        ok: true,
        text: async () => '# Hello',
        headers: { get: () => null }
      });
      globalThis.marked = { parse: vi.fn((md) => `<p>${md}</p>`) };

      const viewer = new DV({ workflowName: 'test-wf' });
      await viewer.showPreview('x-ipe-docs/ideas/project/README.md');

      // Should render in auto/preview mode — no raw pre
      const pre = document.querySelector('.preview-raw-content');
      expect(pre).toBeNull();
    });

    it('hides toggle for non-text file even under /src/', async () => {
      const viewer = new DV({ workflowName: 'test-wf' });
      await viewer.showPreview('x-ipe-docs/ideas/project/src/images/logo.png');

      const toggleBtn = document.querySelector('.preview-toggle-btn');
      expect(toggleBtn).toBeNull();
    });
  });

  describe('CR-002: Toggle interaction', () => {
    let DV;
    beforeEach(() => {
      DV = globalThis.DeliverableViewer;
      globalThis.URL.createObjectURL = vi.fn(() => 'blob:mock-url');
      globalThis.URL.revokeObjectURL = vi.fn();
    });

    it('toggle switches from auto to raw mode', async () => {
      globalThis.fetch = vi.fn().mockResolvedValue({
        ok: true,
        text: async () => '# Hello',
        headers: { get: () => null }
      });
      globalThis.marked = { parse: vi.fn((md) => `<p>${md}</p>`) };

      const viewer = new DV({ workflowName: 'test-wf' });
      await viewer.showPreview('x-ipe-docs/ideas/test/readme.md');

      // Initially in auto mode — no raw pre
      expect(document.querySelector('.preview-raw-content')).toBeNull();

      // Click toggle
      const toggleBtn = document.querySelector('.preview-toggle-btn');
      toggleBtn.click();

      // Now in raw mode
      const pre = document.querySelector('.preview-raw-content');
      expect(pre).not.toBeNull();
      expect(pre.textContent).toBe('# Hello');
    });

    it('toggle does not trigger new fetch', async () => {
      globalThis.fetch = vi.fn().mockResolvedValue({
        ok: true,
        text: async () => '# Hello',
        headers: { get: () => null }
      });
      globalThis.marked = { parse: vi.fn((md) => `<p>${md}</p>`) };

      const viewer = new DV({ workflowName: 'test-wf' });
      await viewer.showPreview('x-ipe-docs/ideas/test/readme.md');

      const fetchCallCount = globalThis.fetch.mock.calls.length;
      const toggleBtn = document.querySelector('.preview-toggle-btn');
      toggleBtn.click();

      expect(globalThis.fetch.mock.calls.length).toBe(fetchCallCount);
    });

    it('opening new file resets to its smart default', async () => {
      globalThis.fetch = vi.fn().mockResolvedValue({
        ok: true,
        text: async () => '# Content',
        headers: { get: () => null }
      });
      globalThis.marked = { parse: vi.fn((md) => `<p>${md}</p>`) };

      const viewer = new DV({ workflowName: 'test-wf' });

      // Open first file and toggle to raw
      await viewer.showPreview('x-ipe-docs/ideas/test/readme.md');
      document.querySelector('.preview-toggle-btn').click();
      expect(document.querySelector('.preview-raw-content')).not.toBeNull();

      // Open second file (non-src) — should be in auto mode
      await viewer.showPreview('x-ipe-docs/ideas/test/doc.md');
      expect(document.querySelector('.preview-raw-content')).toBeNull();
    });
  });
});
