/**
 * Tests for the framework-sync abort messaging (#791 B2 + blocker-C).
 *
 * finalizeStage runs `config.syncFramework()` first, inside a try/catch. If the
 * sync throws, the wizard must:
 *   1. NEVER fall through to "Installation complete" — the error is re-raised so
 *      the process exits non-zero (#791 B2).
 *   2. Classify the failure so recovery advice is accurate (#791 blocker-C):
 *        - ManifestError  → a PRE-sync validation abort; nothing was written, so
 *          the message states "no files were changed".
 *        - any other error → may surface mid-copy, so the message must NOT claim
 *          nothing changed; it warns the state "may be partially applied".
 *
 * We assert on the spinner's stop() message (the user-visible line) and that the
 * original error is re-thrown unchanged in both cases.
 */

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import type { WizardState } from '../types.js';
import type { ConfigService } from '../config/config-service.js';
import { ManifestError } from '../framework/manifest.js';

vi.mock('node:child_process', () => ({
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  spawnSync: vi.fn<any>().mockReturnValue({ status: 0, stdout: '', stderr: '' }),
}));

vi.mock('../platform/detect.js', () => ({
  getShellProfilePath: () => null,
}));

import { finalizeStage } from './finalize.js';

function makeState(mosaicHome: string): WizardState {
  return {
    mosaicHome,
    sourceDir: mosaicHome,
    mode: 'quick',
    installAction: 'keep',
    soul: { agentName: 'TestBot', communicationStyle: 'direct' },
    user: {},
    tools: {},
    runtimes: { detected: [], mcpConfigured: false },
    selectedSkills: [],
  };
}

function buildPrompter() {
  const stop = vi.fn();
  const update = vi.fn();
  const prompter = {
    intro: vi.fn(),
    outro: vi.fn(),
    note: vi.fn(),
    log: vi.fn(),
    warn: vi.fn(),
    text: vi.fn(),
    confirm: vi.fn(),
    select: vi.fn(),
    multiselect: vi.fn(),
    groupMultiselect: vi.fn(),
    spinner: vi.fn().mockReturnValue({ update, stop }),
    separator: vi.fn(),
  };
  return { prompter, stop };
}

function makeConfigService(syncFramework: ConfigService['syncFramework']): ConfigService {
  return {
    readSoul: vi.fn().mockResolvedValue({}),
    readUser: vi.fn().mockResolvedValue({}),
    readTools: vi.fn().mockResolvedValue({}),
    writeSoul: vi.fn().mockResolvedValue(undefined),
    writeUser: vi.fn().mockResolvedValue(undefined),
    writeTools: vi.fn().mockResolvedValue(undefined),
    syncFramework,
    get: vi.fn(),
    set: vi.fn(),
    getSection: vi.fn(),
  } as unknown as ConfigService;
}

describe('finalizeStage — framework sync abort (#791 B2 + blocker-C)', () => {
  let tmp: string;

  beforeEach(() => {
    tmp = mkdtempSync(join(tmpdir(), 'mosaic-sync-abort-'));
  });

  afterEach(() => {
    rmSync(tmp, { recursive: true, force: true });
    vi.clearAllMocks();
  });

  it('re-throws a ManifestError and reports that no files were changed', async () => {
    const err = new ManifestError('Framework manifest defines no usable [framework] paths');
    const { prompter, stop } = buildPrompter();
    const config = makeConfigService(vi.fn().mockRejectedValue(err));

    await expect(finalizeStage(prompter, makeState(tmp), config)).rejects.toBe(err);

    // The abort message must state nothing was written (pre-sync validation).
    expect(stop).toHaveBeenCalledWith(expect.stringContaining('no files were changed'));
    // It must NOT fall through to a success line.
    expect(stop).not.toHaveBeenCalledWith(expect.stringContaining('Installation complete'));
  });

  it('re-throws a non-ManifestError and warns the state may be partially applied', async () => {
    const err = new Error('cp: write error mid-sync (disk full)');
    const { prompter, stop } = buildPrompter();
    const config = makeConfigService(vi.fn().mockRejectedValue(err));

    await expect(finalizeStage(prompter, makeState(tmp), config)).rejects.toBe(err);

    // A generic mid-sync failure must NOT claim nothing changed…
    expect(stop).toHaveBeenCalledWith(expect.stringContaining('may be partially applied'));
    expect(stop).not.toHaveBeenCalledWith(expect.stringContaining('no files were changed'));
    expect(stop).not.toHaveBeenCalledWith(expect.stringContaining('Installation complete'));
  });

  it('does not proceed to config writes when the sync aborts', async () => {
    const err = new ManifestError('malformed manifest');
    const { prompter } = buildPrompter();
    const config = makeConfigService(vi.fn().mockRejectedValue(err));

    await expect(finalizeStage(prompter, makeState(tmp), config)).rejects.toBe(err);

    // writeSoul/writeUser/writeTools are only reached after a successful sync.
    expect(config.writeSoul).not.toHaveBeenCalled();
    expect(config.writeUser).not.toHaveBeenCalled();
    expect(config.writeTools).not.toHaveBeenCalled();
  });
});
