import { mkdtemp, readFile, readdir, writeFile } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';

import { stringify as stringifyYaml } from 'yaml';
import { Command } from 'commander';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { registerPrdyCommand } from './prdy.js';
import { PrdService } from '@mosaicstack/prdy';

// ── Mocks: keep the adapter test offline (no gateway, no disk side effects
//    outside the tmp project dir) ──────────────────────────────────────────────

vi.mock('./with-auth.js', () => ({
  withAuth: vi.fn().mockResolvedValue({
    gateway: 'http://localhost:14242',
    cookie: 'better-auth.session_token=test',
    session: {},
  }),
}));

vi.mock('../tui/gateway-api.js', () => ({
  fetchProjects: vi.fn().mockResolvedValue([]),
}));

// ── Helpers ──────────────────────────────────────────────────────────────────

class ProcessExitError extends Error {
  constructor(readonly code: number) {
    super(`process.exit(${code})`);
  }
}

function stubProcessExit() {
  return vi.spyOn(process, 'exit').mockImplementation(((code?: number) => {
    throw new ProcessExitError(code ?? 0);
  }) as never);
}

const originalCwd = process.cwd();
let projectDir: string;
let errorSpy: ReturnType<typeof vi.spyOn>;
let logSpy: ReturnType<typeof vi.spyOn>;
let exitStub: ReturnType<typeof stubProcessExit>;

function buildTestProgram(): Command {
  const program = new Command('mosaic').exitOverride();
  registerPrdyCommand(program);
  return program;
}

function runPrdy(args: string[]): Promise<unknown> {
  return buildTestProgram().parseAsync(['prdy', ...args], { from: 'user' });
}

function importableDocument(overrides: Record<string, unknown> = {}): Record<string, unknown> {
  return {
    id: 'cmd-import-prd',
    title: 'Command Import PRD',
    status: 'approved', // must be forced to draft: validity is not approval
    projectPath: '/tmp/elsewhere',
    template: 'software',
    version: 1,
    sections: [
      { id: 'introduction', title: 'Introduction', fields: { context: 'x', objective: 'y' } },
    ],
    missions: [],
    createdAt: '2026-01-01T00:00:00.000Z',
    updatedAt: '2026-01-01T00:00:00.000Z',
    ...overrides,
  };
}

beforeEach(async () => {
  projectDir = await mkdtemp(path.join(os.tmpdir(), 'mosaic-prdy-'));
  process.chdir(projectDir);
  exitStub = stubProcessExit();
  errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
  logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
});

afterEach(() => {
  // Restore only the per-test spies: module factory mocks must keep their
  // implementations for the next test.
  exitStub.mockRestore();
  errorSpy.mockRestore();
  logSpy.mockRestore();
  process.chdir(originalCwd);
});

// ── Tests ────────────────────────────────────────────────────────────────────

describe('mosaic prdy (thin adapter over PrdService)', () => {
  it('non-interactive --init creates a PRD in the docs/prdy authority store', async () => {
    await runPrdy(['--init', 'Adapter Created']);

    const files = await readdir(path.join(projectDir, 'docs', 'prdy'));
    expect(files).toHaveLength(1);
    expect(files[0]).toMatch(/\.yaml$/);

    const docs = await new PrdService({ projectPath: projectDir }).list();
    expect(docs).toHaveLength(1);
    expect(docs[0]?.title).toBe('Adapter Created');
    expect(docs[0]?.version).toBe(1);
    expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('PRD created'));
  });

  it('--import <file> creates a valid import through the service', async () => {
    const filePath = path.join(projectDir, 'incoming.yaml');
    await writeFile(filePath, stringifyYaml(importableDocument()), 'utf8');

    await runPrdy(['--import', filePath]);

    const docs = await new PrdService({ projectPath: projectDir }).list();
    expect(docs).toHaveLength(1);
    expect(docs[0]?.id).toBe('cmd-import-prd');
    expect(docs[0]?.status).toBe('draft'); // import ≠ approval
    expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Imported PRD cmd-import-prd'));
  });

  it('--import of a structurally-invalid file is a typed refusal that creates nothing', async () => {
    const filePath = path.join(projectDir, 'broken.yaml');
    await writeFile(filePath, stringifyYaml({ id: 'incomplete', no: 'structure' }), 'utf8');

    await expect(runPrdy(['--import', filePath])).rejects.toBeInstanceOf(ProcessExitError);

    // Typed refusal surfaced to the user, nothing created.
    expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('PRD wizard failed'));
    await expect(readdir(path.join(projectDir, 'docs'))).rejects.toMatchObject({ code: 'ENOENT' });
  });

  it('--import on conflict refuses with a successor proposal and leaves bytes untouched', async () => {
    const service = new PrdService({ projectPath: projectDir });
    const existing = await service.create({ name: 'Conflict Target' });
    const storeFile = path.join(projectDir, 'docs', 'prdy', `${existing.id}.yaml`);
    const beforeBytes = await readFile(storeFile, 'utf8');

    const filePath = path.join(projectDir, 'divergent.yaml');
    await writeFile(
      filePath,
      stringifyYaml(
        importableDocument({
          ...existing,
          title: 'Divergent Command Import',
        }),
      ),
      'utf8',
    );

    await expect(runPrdy(['--import', filePath])).rejects.toBeInstanceOf(ProcessExitError);

    expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('refusing to overwrite'));
    expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('--accept-successor'));

    // Original authority document is byte-identical on disk.
    expect(await readFile(storeFile, 'utf8')).toBe(beforeBytes);
  });

  it('--import --accept-successor persists the successor version explicitly', async () => {
    const service = new PrdService({ projectPath: projectDir });
    const existing = await service.create({ name: 'Successor Target' });

    const filePath = path.join(projectDir, 'divergent2.yaml');
    await writeFile(
      filePath,
      stringifyYaml(
        importableDocument({
          ...existing,
          title: 'Accepted Via CLI',
        }),
      ),
      'utf8',
    );

    await runPrdy(['--import', filePath, '--accept-successor']);

    const doc = await service.get(existing.id);
    expect(doc.version).toBe(2);
    expect(doc.title).toBe('Accepted Via CLI');
    expect(doc.status).toBe('draft');
    expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('successor'));
  });

  it('--export writes a labeled generated view and never touches authority', async () => {
    const service = new PrdService({ projectPath: projectDir });
    const created = await service.create({ name: 'Export Via CLI' });
    const before = await service.get(created.id);

    await runPrdy(['--export', created.id]);

    const mdPath = path.join(projectDir, 'docs', 'prdy', `${created.id}.md`);
    const md = await readFile(mdPath, 'utf8');
    expect(md).toContain('generated view — do not edit');
    expect(md).toContain(`prd-id: ${created.id}`);
    expect(md).toContain('prd-version: 1');
    expect(logSpy).toHaveBeenCalledWith(
      expect.stringContaining(`Generated view written: ${mdPath}`),
    );

    // Authority unchanged by the export.
    expect(await service.get(created.id)).toEqual(before);
  });
});
