/**
 * @vitest-environment happy-dom
 */
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MemoryRouter } from 'react-router-dom';
import BuilderPage, { formatBytes } from './BuilderPage';
import { ForgeProvider } from '../context/ForgeContext';
import { routerFuture } from '../routerFuture';
import { mockServerConfig, mockServerInfo } from '../test/fixtures';
import { api } from '../api/client';

function renderBuilder(initialEntries = ['/forge']) {
  return render(
    <MemoryRouter initialEntries={initialEntries} future={routerFuture}>
      <ForgeProvider>
        <BuilderPage />
      </ForgeProvider>
    </MemoryRouter>
  );
}

function missionWizardScope() {
  const root = screen.getByText('MISSION RITUAL — 3-STEP WIZARD').closest('.forge-mission-wizard');
  if (!root) throw new Error('Mission wizard not found');
  return within(root as HTMLElement);
}

function pathForgeSection() {
  const root = screen.getByText('PATH FORGE — Recursive Batch Seed').closest('.form-section');
  if (!root) throw new Error('Path forge section not found');
  return within(root as HTMLElement);
}

function fusionBatchFileInput() {
  const label = screen.getByText(/Batch fusion — fuse many files at once/i);
  const formGroup = label.closest('.form-group');
  if (!formGroup) throw new Error('Batch fusion form group not found');
  const input = formGroup.querySelector('input[type="file"][multiple]') as HTMLInputElement | null;
  if (!input) throw new Error('Batch fusion file input not found');
  return input;
}

function mockFusionFile(name: string, content = 'payload') {
  return new File([content], name, { type: 'application/octet-stream' });
}

type BuildResult = Awaited<ReturnType<typeof api.buildAgent>>;

function successfulBuild(overrides: Partial<BuildResult> = {}): BuildResult {
  return {
    success: true,
    file_name: 'worker-1.exe',
    file_size: 4 * 1024 * 1024,
    download_url: '/api/v1/builds/test/download',
    fusion_enabled: false,
    obfuscated: false,
    signed: false,
    ...overrides,
  };
}

describe('formatBytes', () => {
  it('formats sub-kilobyte values as bytes', () => {
    expect(formatBytes(512)).toBe('512 B');
  });

  it('formats kilobytes and megabytes with two decimals', () => {
    expect(formatBytes(2048)).toBe('2.00 KB');
    expect(formatBytes(5 * 1024 * 1024)).toBe('5.00 MB');
  });

  it('formats gigabytes and terabytes', () => {
    expect(formatBytes(3 * 1024 * 1024 * 1024)).toBe('3.00 GB');
    expect(formatBytes(2 * 1024 * 1024 * 1024 * 1024)).toBe('2.00 TB');
  });
});

describe('BuilderPage', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    localStorage.clear();
    vi.spyOn(api, 'getConfig').mockResolvedValue(mockServerConfig());
    vi.spyOn(api, 'getServerInfo').mockResolvedValue(mockServerInfo);
    vi.spyOn(api, 'listBuilds').mockResolvedValue([]);
    vi.spyOn(api, 'buildAgent').mockResolvedValue({
      success: true,
      file_name: 'worker-1.exe',
      file_size: 4 * 1024 * 1024,
      download_url: '/api/v1/builds/test/download',
      fusion_enabled: false,
      obfuscated: false,
      signed: false,
    });
    vi.spyOn(api, 'listBlueprints').mockResolvedValue([]);
    vi.spyOn(api, 'saveBlueprint').mockResolvedValue({ name: 'my-blueprint', size: 1024, created_at: '' });
    vi.spyOn(api, 'cancelBuild').mockResolvedValue({ ok: true });
  });

  afterEach(() => {
    vi.unstubAllGlobals();
    cleanup();
  });

  it('shows loading hero before defaults arrive', () => {
    renderBuilder();
    expect(screen.getByRole('heading', { level: 1, name: 'Forge' })).toBeInTheDocument();
    expect(screen.getByText('Loading forge defaults from server...')).toBeInTheDocument();
  });

  it('renders forge form with identity field labels after load', async () => {
    renderBuilder();
    expect(await screen.findByRole('heading', { level: 2, name: 'Quick Forge' })).toBeInTheDocument();
    expect(screen.getByText('INSTALLER FORGE')).toBeInTheDocument();
    expect(screen.getByText('Worker Name')).toBeInTheDocument();
    expect(screen.getByText('Control Endpoint')).toBeInTheDocument();
    expect(screen.getByText('XMR Wallet Address')).toBeInTheDocument();
    expect(api.getConfig).toHaveBeenCalled();
    expect(api.getServerInfo).toHaveBeenCalled();
    expect(api.listBuilds).toHaveBeenCalled();
  });

  it('surfaces load failure when server info cannot be fetched', async () => {
    vi.spyOn(api, 'getConfig').mockRejectedValue(new Error('offline'));
    renderBuilder();
    expect(
      await screen.findByText('Failed to load server config — is the control server running?')
    ).toBeInTheDocument();
  });

  it('shows LAN endpoint chips from server info', async () => {
    renderBuilder();
    await screen.findByRole('heading', { level: 2, name: 'Quick Forge' });
    expect(screen.getByRole('button', { name: mockServerInfo.suggested_url! })).toBeInTheDocument();
  });

  it('switches to advanced mode and reveals pool configuration', async () => {
    renderBuilder();
    await screen.findByRole('heading', { level: 2, name: 'Quick Forge' });
    await userEvent.setup().click(screen.getByRole('button', { name: 'Advanced' }));
    expect(await screen.findByRole('heading', { level: 2, name: 'Build Miner Installer' })).toBeInTheDocument();
    expect(screen.getByText('FORGE RULES — READ THIS ONCE')).toBeInTheDocument();
    expect(screen.getByText('Mining Pools')).toBeInTheDocument();
  });

  it('submits forge when preflight passes', async () => {
    const buildSpy = vi.spyOn(api, 'buildAgent');
    renderBuilder();
    await screen.findByRole('button', { name: /FORGE INSTALLER/i });
    await userEvent.setup().click(screen.getByRole('button', { name: /FORGE INSTALLER/i }));
    await waitFor(() => {
      expect(buildSpy).toHaveBeenCalled();
    });
    expect(await screen.findByText('worker-1.exe')).toBeInTheDocument();
    expect(screen.getByRole('button', { name: /Download/i })).toBeInTheDocument();
  });

  it('offers forge-next-worker after a successful build', async () => {
    renderBuilder();
    await screen.findByRole('button', { name: /FORGE INSTALLER/i });
    await userEvent.setup().click(screen.getByRole('button', { name: /FORGE INSTALLER/i }));
    expect(await screen.findByText('worker-1.exe')).toBeInTheDocument();

    await userEvent.setup().click(screen.getByRole('button', { name: 'Forge next worker' }));
    expect(screen.queryByText('worker-1.exe')).not.toBeInTheDocument();
    expect(screen.getByPlaceholderText(/office-pc-1/i)).toHaveValue('worker-2');
  });

  it('shows fleet blueprint save prompt after first successful forge', async () => {
    const promptMock = vi.fn().mockReturnValue('fleet-default');
    vi.stubGlobal('prompt', promptMock);
    const saveSpy = vi.spyOn(api, 'saveBlueprint');

    renderBuilder();
    await screen.findByRole('button', { name: /FORGE INSTALLER/i });
    await userEvent.setup().click(screen.getByRole('button', { name: /FORGE INSTALLER/i }));
    expect(await screen.findByText('worker-1.exe')).toBeInTheDocument();

    await userEvent.setup().click(screen.getByRole('button', { name: 'Save as fleet blueprint' }));
    await waitFor(() => {
      expect(promptMock).toHaveBeenCalledWith('Save this setup as a fleet blueprint:', 'fleet-default');
      expect(saveSpy).toHaveBeenCalledWith('fleet-default', expect.any(Object));
    });
  });

  it('blocks forge when preflight has errors (empty wallet)', async () => {
    renderBuilder();
    const wallet = await screen.findByDisplayValue(/^4A+/);
    fireEvent.change(wallet, { target: { value: '' } });
    const forgeBtn = screen.getByRole('button', { name: /FIX .* ERROR/i });
    expect(forgeBtn).toBeDisabled();
    expect(screen.getByText('PREFLIGHT CROSS-CHECK')).toBeInTheDocument();
  });

  it('persists simple vs advanced mode in localStorage', async () => {
    renderBuilder();
    await screen.findByRole('heading', { level: 2, name: 'Quick Forge' });
    await userEvent.setup().click(screen.getByRole('button', { name: 'Advanced' }));
    expect(localStorage.getItem('aetherforge-forge-mode')).toBe('advanced');
    await userEvent.setup().click(screen.getByRole('button', { name: 'Simple' }));
    expect(localStorage.getItem('aetherforge-forge-mode')).toBe('simple');
  });

  it('loads blueprint list when Load Blueprint is clicked', async () => {
    vi.spyOn(api, 'listBlueprints').mockResolvedValue([
      { name: 'office-preset', size: 2048, created_at: '2026-05-30T12:00:00Z' },
    ]);
    renderBuilder();
    await screen.findByRole('button', { name: /Load Blueprint/i });
    await userEvent.setup().click(screen.getByRole('button', { name: /Load Blueprint/i }));
    expect(await screen.findByText('Saved Blueprints')).toBeInTheDocument();
    expect(screen.getByText('office-preset')).toBeInTheDocument();
    expect(api.listBlueprints).toHaveBeenCalled();
  });

  it('shows fusion deliverable hint when Fusion card is selected', async () => {
    renderBuilder();
    await screen.findByRole('heading', { level: 2, name: 'Quick Forge' });
    await userEvent.setup().click(
      screen.getByRole('button', { name: /Hide miner in any file/i })
    );
    expect(
      screen.getByText(/Drop any file — PDF, video, document/i)
    ).toBeInTheDocument();
  });

  it('applies forge skin class from default operation mode', async () => {
    localStorage.setItem('aetherforge-operation-mode', 'ghost_walk');
    localStorage.setItem('aetherforge-forge-theme', 'auto');
    const { container } = renderBuilder();
    await screen.findByRole('heading', { level: 2, name: 'Quick Forge' });
    expect(container.querySelector('.command-deck')).toHaveClass('forge-skin--ghost');
  });

  it('honors forge theme override over operation mode', async () => {
    localStorage.setItem('aetherforge-operation-mode', 'ghost_walk');
    localStorage.setItem('aetherforge-forge-theme', 'halloween');
    const { container } = renderBuilder();
    await screen.findByRole('heading', { level: 2, name: 'Quick Forge' });
    expect(container.querySelector('.command-deck')).toHaveClass('forge-skin--halloween');
    expect(container.querySelector('.command-deck')).not.toHaveClass('forge-skin--ghost');
  });

  it('updates forge skin when operation mode chip is selected', async () => {
    localStorage.setItem('aetherforge-forge-theme', 'auto');
    const { container } = renderBuilder();
    await screen.findByRole('heading', { level: 2, name: 'Quick Forge' });
    await userEvent.setup().click(screen.getByRole('button', { name: 'Wildfire' }));
    expect(container.querySelector('.command-deck')).toHaveClass('forge-skin--wildfire');
  });

  it('applies crucible skin for Crucible Storm mode', async () => {
    localStorage.setItem('aetherforge-operation-mode', 'crucible_storm');
    localStorage.setItem('aetherforge-forge-theme', 'auto');
    const { container } = renderBuilder();
    await screen.findByRole('heading', { level: 2, name: 'Quick Forge' });
    expect(container.querySelector('.command-deck')).toHaveClass('forge-skin--crucible');
  });

  it('applies halloween skin for Sigil Mask mode', async () => {
    localStorage.setItem('aetherforge-operation-mode', 'sigil_mask');
    localStorage.setItem('aetherforge-forge-theme', 'auto');
    const { container } = renderBuilder();
    await screen.findByRole('heading', { level: 2, name: 'Quick Forge' });
    expect(container.querySelector('.command-deck')).toHaveClass('forge-skin--halloween');
  });

  it('renders mission ritual wizard with three step pills', async () => {
    renderBuilder();
    await screen.findByRole('heading', { level: 2, name: 'Quick Forge' });
    const wizard = missionWizardScope();
    expect(screen.getByText('MISSION RITUAL — 3-STEP WIZARD')).toBeInTheDocument();
    expect(wizard.getByRole('tab', { name: /Mode/i })).toBeInTheDocument();
    expect(wizard.getByRole('tab', { name: /Profile/i })).toBeInTheDocument();
    expect(wizard.getByRole('tab', { name: /Launch/i })).toBeInTheDocument();
    expect(wizard.getByRole('button', { name: 'Ghost' })).toBeInTheDocument();
    expect(wizard.getByRole('button', { name: 'Loud' })).toBeInTheDocument();
    expect(wizard.getByRole('button', { name: 'Spread' })).toBeInTheDocument();
  });

  it('advances mission wizard from mode to profile to launch', async () => {
    const user = userEvent.setup();
    renderBuilder();
    await screen.findByRole('heading', { level: 2, name: 'Quick Forge' });
    const wizard = missionWizardScope();

    await user.click(wizard.getByRole('button', { name: 'Spread' }));
    await user.click(wizard.getByRole('button', { name: 'Next →' }));
    expect(wizard.getByText('Spread profile (optional)')).toBeInTheDocument();

    await user.click(wizard.getByRole('button', { name: 'LAN Kindling' }));
    await user.click(wizard.getByRole('button', { name: 'Next →' }));
    expect(wizard.getByRole('button', { name: '🚀 Launch Ritual' })).toBeInTheDocument();
    expect(wizard.getByLabelText('Campaign slug (?c=)')).toBeInTheDocument();
  });

  it('runs mission ritual and shows success modal with spread landing link', async () => {
    vi.spyOn(api, 'buildAgent').mockResolvedValue({
      success: true,
      build_id: 'ritual-build-1',
      file_name: 'worker-ritual.exe',
      file_size: 4096,
      download_url: '/api/v1/builds/ritual-build-1/download',
      fusion_enabled: false,
      obfuscated: false,
      signed: false,
    });
    vi.spyOn(api, 'exportSpreadKit').mockResolvedValue(undefined);

    const user = userEvent.setup();
    renderBuilder();
    await screen.findByRole('heading', { level: 2, name: 'Quick Forge' });
    const wizard = missionWizardScope();

    await user.click(wizard.getByRole('button', { name: 'Spread' }));
    await user.click(wizard.getByRole('button', { name: 'Next →' }));
    await user.click(wizard.getByRole('button', { name: 'LAN Kindling' }));
    await user.click(wizard.getByRole('button', { name: 'Next →' }));
    await user.click(wizard.getByRole('button', { name: '🚀 Launch Ritual' }));

    expect(await screen.findByText('Mission complete')).toBeInTheDocument();
    expect(screen.getByRole('button', { name: /View install commands in Builds/i })).toBeInTheDocument();
    expect(screen.getByRole('link', { name: 'Open spread landing' })).toHaveAttribute('href', '/spread/');
    expect(api.buildAgent).toHaveBeenCalled();
    expect(api.exportSpreadKit).toHaveBeenCalled();
  });

  it('APK mode toggle locks platform to Android arm64 and hides Fusion', async () => {
    localStorage.setItem('aetherforge-forge-mode', 'advanced');
    const user = userEvent.setup();
    renderBuilder();
    await screen.findByText('Worker Name');

    const apkToggle = await screen.findByRole('checkbox', { name: /APK mode/i });
    await user.click(apkToggle);

    expect(screen.getByDisplayValue('Android (arm64)')).toBeInTheDocument();
    expect(screen.getByText(/platform=android/i)).toBeInTheDocument();
    expect(screen.queryByText('Fusion — Hide miner in any file')).not.toBeInTheDocument();
  });

  it('polls builder progress endpoint while a forge is running', async () => {
    type BuildResult = Awaited<ReturnType<typeof api.buildAgent>>;
    let resolveBuild!: (value: BuildResult) => void;
    vi.spyOn(api, 'buildAgent').mockReturnValue(
      new Promise<BuildResult>((resolve) => {
        resolveBuild = resolve;
      }),
    );

    const fetchMock = vi.fn().mockResolvedValue({
      ok: true,
      json: async () => ({ stage: 'Compiling agent', pct: 42 }),
    });
    vi.stubGlobal('fetch', fetchMock);

    renderBuilder();
    await screen.findByRole('button', { name: /FORGE INSTALLER/i });
    fireEvent.click(screen.getByRole('button', { name: /FORGE INSTALLER/i }));

    await waitFor(
      () => {
        expect(fetchMock).toHaveBeenCalledWith(
          expect.stringMatching(/\/api\/v1\/builder\/progress\//),
          expect.objectContaining({ headers: expect.any(Object) }),
        );
      },
      { timeout: 3000 },
    );

    resolveBuild({
      success: true,
      file_name: 'worker-1.exe',
      file_size: 1024,
      download_url: '/api/v1/builds/test/download',
      fusion_enabled: false,
      obfuscated: false,
      signed: false,
    });
    await waitFor(() => expect(screen.getByText('worker-1.exe')).toBeInTheDocument());
  });

  it('calls cancelBuild when Kill Build is clicked during single forge', async () => {
    type BuildResult = Awaited<ReturnType<typeof api.buildAgent>>;
    vi.spyOn(api, 'buildAgent').mockReturnValue(new Promise<BuildResult>(() => {}));

    const cancelSpy = vi.spyOn(api, 'cancelBuild').mockResolvedValue({ ok: true });
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
      ok: true,
      json: async () => ({ stage: 'Compiling', pct: 10 }),
    }));

    renderBuilder();
    await screen.findByRole('button', { name: /FORGE INSTALLER/i });
    fireEvent.click(screen.getByRole('button', { name: /FORGE INSTALLER/i }));

    const killBtn = await screen.findByRole('button', { name: /Kill Build/i });
    fireEvent.click(killBtn);

    await waitFor(() => {
      expect(cancelSpy).toHaveBeenCalled();
    });
  });

  it('submits path forge and shows placement summary', async () => {
    localStorage.setItem('aetherforge-forge-mode', 'advanced');
    const fetchMock = vi.fn().mockResolvedValue({
      ok: true,
      json: async () => ({
        success: true,
        placed: 3,
        total: 1,
        skipped: 2,
        errors: 0,
        results: [{ source: 'movie.mkv', files: ['movie.bat', 'click_bat_to_unlock_movie'] }],
      }),
    });
    vi.stubGlobal('fetch', fetchMock);

    renderBuilder();
    await screen.findByRole('heading', { level: 2, name: 'Build Miner Installer' });

    const pathForgeSection = screen.getByText('PATH FORGE — Recursive Batch Seed').closest('.form-section') as HTMLElement;
    const pathInput = within(pathForgeSection).getByPlaceholderText(/E:\\Movies/i);
    fireEvent.change(pathInput, { target: { value: 'D:\\Movies' } });
    fireEvent.click(screen.getByRole('button', { name: /LAUNCH PATH FORGE/i }));

    expect(await screen.findByText(/3 files placed across 1 source files/i)).toBeInTheDocument();
    expect(fetchMock).toHaveBeenCalledWith(
      '/api/v1/builder/path-forge',
      expect.objectContaining({ method: 'POST' }),
    );
  });

  it('shows path forge busy state while seeding', async () => {
    localStorage.setItem('aetherforge-forge-mode', 'advanced');
    let resolveForge!: (value: Response) => void;
    const fetchMock = vi.fn().mockReturnValue(
      new Promise<Response>((resolve) => {
        resolveForge = resolve;
      }),
    );
    vi.stubGlobal('fetch', fetchMock);

    renderBuilder();
    await screen.findByRole('heading', { level: 2, name: 'Build Miner Installer' });

    const section = pathForgeSection();
    fireEvent.change(section.getByPlaceholderText(/E:\\Movies/i), {
      target: { value: 'D:\\Movies' },
    });
    fireEvent.click(screen.getByRole('button', { name: /LAUNCH PATH FORGE/i }));

    expect(await screen.findByRole('button', { name: /Seeding/i })).toBeDisabled();

    resolveForge({
      ok: true,
      json: async () => ({ success: true, placed: 1, total: 1, errors: 0, results: [] }),
    } as Response);

    await waitFor(() => {
      expect(screen.getByRole('button', { name: /LAUNCH PATH FORGE/i })).not.toBeDisabled();
    });
  });

  it('calls cancelBuild on unmount when single forge is in flight', async () => {
    const capturedTokens: string[] = [];
    vi.spyOn(api, 'buildAgent').mockImplementation((req) => {
      capturedTokens.push(req.cancel_token ?? '');
      return new Promise<BuildResult>(() => {});
    });
    const cancelSpy = vi.spyOn(api, 'cancelBuild').mockResolvedValue({ ok: true });
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
      ok: true,
      json: async () => ({ stage: 'Compiling', pct: 10 }),
    }));

    const { unmount } = renderBuilder();
    await screen.findByRole('button', { name: /FORGE INSTALLER/i });
    fireEvent.click(screen.getByRole('button', { name: /FORGE INSTALLER/i }));
    await waitFor(() => expect(api.buildAgent).toHaveBeenCalled());

    unmount();

    await waitFor(() => {
      expect(cancelSpy).toHaveBeenCalledWith(capturedTokens[0]);
    });
  });

  it('path forge blocks duplicate launch while seeding (race guard)', async () => {
    localStorage.setItem('aetherforge-forge-mode', 'advanced');
    let resolveForge!: (value: Response) => void;
    const fetchMock = vi.fn().mockReturnValue(
      new Promise<Response>((resolve) => {
        resolveForge = resolve;
      }),
    );
    vi.stubGlobal('fetch', fetchMock);

    renderBuilder();
    await screen.findByRole('heading', { level: 2, name: 'Build Miner Installer' });

    const section = pathForgeSection();
    fireEvent.change(section.getByPlaceholderText(/E:\\Movies/i), { target: { value: 'D:\\Movies' } });
    const launchBtn = screen.getByRole('button', { name: /LAUNCH PATH FORGE/i });
    fireEvent.click(launchBtn);

    const seedingBtn = await screen.findByRole('button', { name: /Seeding/i });
    expect(seedingBtn).toBeDisabled();
    fireEvent.click(seedingBtn);
    expect(fetchMock).toHaveBeenCalledTimes(1);

    resolveForge({
      ok: true,
      json: async () => ({ success: true, placed: 1, total: 1, errors: 0, results: [] }),
    } as Response);
    await waitFor(() => expect(screen.getByRole('button', { name: /LAUNCH PATH FORGE/i })).not.toBeDisabled());
  });

  it('path forge unmount during seeding does not throw', async () => {
    localStorage.setItem('aetherforge-forge-mode', 'advanced');
    vi.stubGlobal('fetch', vi.fn().mockReturnValue(new Promise<Response>(() => {})));

    const { unmount } = renderBuilder();
    await screen.findByRole('heading', { level: 2, name: 'Build Miner Installer' });

    const section = pathForgeSection();
    fireEvent.change(section.getByPlaceholderText(/E:\\Movies/i), { target: { value: 'D:\\Movies' } });
    fireEvent.click(screen.getByRole('button', { name: /LAUNCH PATH FORGE/i }));
    await screen.findByRole('button', { name: /Seeding/i });

    expect(() => unmount()).not.toThrow();
  });

  it('batch forge processes multiple files with per-file preflight and cancel_token', async () => {
    const user = userEvent.setup();
    const buildSpy = vi.spyOn(api, 'buildAgent').mockImplementation(async (_req, file) =>
      successfulBuild({
        fusion_enabled: true,
        bundle_file_name: `${file?.name ?? 'file'}-package.zip`,
        fusion_export_dir: `fusion-deliverables/${file?.name ?? 'file'}`,
      }),
    );
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
      ok: true,
      json: async () => ({ stage: 'Packaging', pct: 80 }),
    }));

    renderBuilder();
    await screen.findByRole('heading', { level: 2, name: 'Quick Forge' });
    await user.click(screen.getByRole('button', { name: /Hide miner in any file/i }));

    const alpha = mockFusionFile('alpha.pdf');
    const beta = mockFusionFile('beta.mkv');
    fireEvent.change(fusionBatchFileInput(), { target: { files: [alpha, beta] } });
    expect(await screen.findByText(/2 files queued/i)).toBeInTheDocument();

    await user.click(screen.getByRole('button', { name: /Forge all 2 files/i }));

    await waitFor(() => expect(buildSpy).toHaveBeenCalledTimes(2));
    expect(buildSpy.mock.calls[0][1]?.name).toBe('alpha.pdf');
    expect(buildSpy.mock.calls[1][1]?.name).toBe('beta.mkv');
    expect(buildSpy.mock.calls[0][0].fusion_enabled).toBe(true);
    expect(buildSpy.mock.calls[0][0].cancel_token).toBeTruthy();
    expect(buildSpy.mock.calls[1][0].cancel_token).toBeTruthy();
    expect(await screen.findByText(/Batch forged 2 file/i)).toBeInTheDocument();
    expect(screen.getByText(/2\/2 — done/i)).toBeInTheDocument();
  });

  it('cancels batch forge mid-loop via Cancel Batch and calls cancelBuild', async () => {
    const user = userEvent.setup();
    let resolveFirst!: (value: BuildResult) => void;
    const capturedTokens: string[] = [];
    const buildSpy = vi.spyOn(api, 'buildAgent').mockImplementation((req) => {
      capturedTokens.push(req.cancel_token ?? '');
      if (capturedTokens.length === 1) {
        return new Promise<BuildResult>((resolve) => {
          resolveFirst = resolve;
        });
      }
      return Promise.resolve(successfulBuild({ fusion_enabled: true }));
    });
    const cancelSpy = vi.spyOn(api, 'cancelBuild').mockResolvedValue({ ok: true });
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
      ok: true,
      json: async () => ({ stage: 'Compiling', pct: 35 }),
    }));

    renderBuilder();
    await screen.findByRole('heading', { level: 2, name: 'Quick Forge' });
    await user.click(screen.getByRole('button', { name: /Hide miner in any file/i }));

    fireEvent.change(fusionBatchFileInput(), {
      target: { files: [mockFusionFile('one.pdf'), mockFusionFile('two.pdf')] },
    });
    await user.click(screen.getByRole('button', { name: /Forge all 2 files/i }));
    await waitFor(() => expect(buildSpy).toHaveBeenCalledTimes(1));

    fireEvent.click(await screen.findByRole('button', { name: /Cancel Batch/i }));
    expect(cancelSpy).toHaveBeenCalledWith(capturedTokens[0]);

    resolveFirst(successfulBuild({
      fusion_enabled: true,
      bundle_file_name: 'one-package.zip',
      fusion_export_dir: 'fusion-deliverables/one.pdf',
    }));

    expect(await screen.findByText(/Batch forge cancelled after 1 of 2 file/i)).toBeInTheDocument();
    await waitFor(() => expect(buildSpy).toHaveBeenCalledTimes(1));
    expect(screen.getByText(/1\/2 — cancelled/i)).toBeInTheDocument();
  });

  it('skips per-file build failures and shows skipped counter in batch progress', async () => {
    const user = userEvent.setup();
    const buildSpy = vi.spyOn(api, 'buildAgent').mockImplementation(async (_req, file) => {
      if (file?.name === 'bad.pdf') {
        return { success: false, error: 'Payload too large for fusion stub' };
      }
      return successfulBuild({
        fusion_enabled: true,
        bundle_file_name: `${file?.name ?? 'file'}-package.zip`,
        fusion_export_dir: `fusion-deliverables/${file?.name ?? 'file'}`,
      });
    });
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
      ok: true,
      json: async () => ({ stage: 'Packaging', pct: 90 }),
    }));

    renderBuilder();
    await screen.findByRole('heading', { level: 2, name: 'Quick Forge' });
    await user.click(screen.getByRole('button', { name: /Hide miner in any file/i }));

    fireEvent.change(fusionBatchFileInput(), {
      target: { files: [mockFusionFile('bad.pdf'), mockFusionFile('good.pdf')] },
    });
    await user.click(screen.getByRole('button', { name: /Forge all 2 files/i }));

    await waitFor(() => expect(buildSpy).toHaveBeenCalledTimes(2));
    expect(await screen.findByText(/Batch forged 1 file\(s\) · 1 skipped/i)).toBeInTheDocument();
    expect(screen.getByText(/bad\.pdf — Payload too large for fusion stub/i)).toBeInTheDocument();
    expect(screen.getByText(/2\/2 — done · 1 skipped/i)).toBeInTheDocument();
    expect(screen.getByText(/good\.pdf — Saved → fusion-deliverables\/good\.pdf/i)).toBeInTheDocument();
  });

  it('skips per-file preflight failures without aborting the batch', async () => {
    const user = userEvent.setup();
    const forgeValidation = await import('../help/forgeValidation');
    const { runForgePreflight: realPreflight } = await vi.importActual<
      typeof import('../help/forgeValidation')
    >('../help/forgeValidation');
    const preflightSpy = vi.spyOn(forgeValidation, 'runForgePreflight').mockImplementation((form, fusion) => {
      if (form.fusion_media_base_name === 'blocked.pdf') {
        return [{ id: 'fusion', level: 'error', message: 'Fusion media exceeds size cap' }];
      }
      return realPreflight(form, fusion);
    });
    const buildSpy = vi.spyOn(api, 'buildAgent').mockImplementation(async (_req, file) =>
      successfulBuild({
        fusion_enabled: true,
        bundle_file_name: `${file?.name ?? 'file'}-package.zip`,
        fusion_export_dir: `fusion-deliverables/${file?.name ?? 'file'}`,
      }),
    );
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
      ok: true,
      json: async () => ({ stage: 'Packaging', pct: 90 }),
    }));

    renderBuilder();
    await screen.findByRole('heading', { level: 2, name: 'Quick Forge' });
    await user.click(screen.getByRole('button', { name: /Hide miner in any file/i }));

    fireEvent.change(fusionBatchFileInput(), {
      target: { files: [mockFusionFile('blocked.pdf'), mockFusionFile('allowed.pdf')] },
    });
    await user.click(screen.getByRole('button', { name: /Forge all 2 files/i }));

    await waitFor(() => expect(buildSpy).toHaveBeenCalledTimes(1));
    expect(buildSpy.mock.calls[0][1]?.name).toBe('allowed.pdf');
    expect(await screen.findByText(/blocked\.pdf — Fusion media exceeds size cap/i)).toBeInTheDocument();
    expect(screen.getByText(/2\/2 — done · 1 skipped/i)).toBeInTheDocument();
    preflightSpy.mockRestore();
  });

  it('selects fleet role seeder chip in advanced spread section', async () => {
    const buildSpy = vi.spyOn(api, 'buildAgent');
    const user = userEvent.setup();
    renderBuilder();
    await screen.findByRole('heading', { level: 2, name: 'Quick Forge' });
    await user.click(screen.getByRole('button', { name: 'Advanced' }));
    await screen.findByRole('heading', { level: 2, name: 'Build Miner Installer' });

    const seederChip = screen.getByRole('button', { name: 'Seeder' });
    expect(seederChip).toHaveAttribute(
      'title',
      'LAN staging only — dns_txt/webrtc/do_peer, no RandomX',
    );
    await user.click(seederChip);
    expect(seederChip.className).toMatch(/active/);

    await user.click(screen.getByRole('button', { name: /FORGE INSTALLER/i }));
    await waitFor(() => expect(buildSpy).toHaveBeenCalled());
    expect(buildSpy.mock.calls[0][0].fleet_role).toBe('seeder');
    expect(screen.getByText(/Seeder skips mining/i)).toBeInTheDocument();
  });

  it('clears fusion batch queue after successful batch forge', async () => {
    const user = userEvent.setup();
    vi.spyOn(api, 'buildAgent').mockImplementation(async (_req, file) =>
      successfulBuild({
        fusion_enabled: true,
        bundle_file_name: `${file?.name ?? 'file'}-package.zip`,
        fusion_export_dir: `fusion-deliverables/${file?.name ?? 'file'}`,
      }),
    );
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
      ok: true,
      json: async () => ({ stage: 'Packaging', pct: 90 }),
    }));

    renderBuilder();
    await screen.findByRole('heading', { level: 2, name: 'Quick Forge' });
    await user.click(screen.getByRole('button', { name: /Hide miner in any file/i }));
    fireEvent.change(fusionBatchFileInput(), {
      target: { files: [mockFusionFile('solo.pdf')] },
    });
    expect(await screen.findByText(/1 file queued/i)).toBeInTheDocument();

    await user.click(screen.getByRole('button', { name: /Forge all 1 file/i }));
    await waitFor(() => expect(screen.getByText(/Batch forged 1 file/i)).toBeInTheDocument());
    expect(screen.queryByText(/1 file queued/i)).not.toBeInTheDocument();
    expect(screen.queryByRole('button', { name: /Cancel Batch/i })).not.toBeInTheDocument();
  });
});
