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

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

import { registerMissionCommand } from './mission.js';
import { PrdService } from '@mosaicstack/prdy';
import type { MissionInfo } from '../tui/gateway-api.js';

// ── Mocks: the gateway is not available in adapter tests ──────────────────────

// vi.hoisted: the mock factory is hoisted above imports, so the fixture must
// be initialized there too.
const MISSION = vi.hoisted(
  (): MissionInfo => ({
    id: 'mission-plan-1',
    name: 'Plan Mission Alpha',
    description: null,
    status: 'planning',
    projectId: null,
    userId: null,
    phase: null,
    milestones: null,
    config: null,
    createdAt: '2026-01-01T00:00:00.000Z',
    updatedAt: '2026-03-04T05:06:07.000Z',
  }),
);

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', () => ({
  fetchMissions: vi.fn().mockResolvedValue([MISSION]),
  fetchMission: vi.fn(),
  createMission: vi.fn(),
  updateMission: vi.fn(),
  fetchMissionTasks: vi.fn().mockResolvedValue([]),
  createMissionTask: vi.fn(),
  updateMissionTask: vi.fn(),
  fetchProjects: vi.fn().mockResolvedValue([]),
}));

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

const originalCwd = process.cwd();
let projectDir: string;
let logSpy: ReturnType<typeof vi.spyOn>;
let consoleStub: ReturnType<typeof vi.spyOn>[] = [];

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

beforeEach(async () => {
  projectDir = await mkdtemp(path.join(os.tmpdir(), 'mosaic-mission-plan-'));
  process.chdir(projectDir);
  logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
  consoleStub.push(logSpy);
});

afterEach(() => {
  // Restore only the per-test spies; module factory mocks keep their
  // implementations across tests.
  for (const stub of consoleStub) stub.mockRestore();
  consoleStub = [];
  process.chdir(originalCwd);
});

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

describe('mosaic mission --plan (thin adapter over PrdService)', () => {
  it('creates the PRD in the shared docs/prdy authority store and persists the mission linkage', async () => {
    await buildTestProgram().parseAsync(['mission', '--plan', 'Plan Mission Alpha'], {
      from: 'user',
    });

    // PRD landed in the same store `mosaic prdy` uses.
    const files = await readdir(path.join(projectDir, 'docs', 'prdy'));
    expect(files).toHaveLength(1);
    expect(files[0]).toMatch(/\.yaml$/);

    // Fresh service instance (new-process equivalent) reads the linkage back.
    const service = new PrdService({ projectPath: projectDir });
    const docs = await service.list();
    expect(docs).toHaveLength(1);

    const prd = docs[0]!;
    expect(prd.title).toBe('Plan Mission Alpha');
    expect(prd.version).toBe(1);

    const links = await service.listMissionLinks(prd.id);
    expect(links).toHaveLength(1);
    expect(links[0]).toMatchObject({
      missionId: MISSION.id,
      missionVersion: MISSION.updatedAt, // mission version marker
      prdVersion: 1,
    });

    expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('PRD created and linked'));
  });

  it('linkage is persisted in the YAML authority document itself (survives restart)', async () => {
    await buildTestProgram().parseAsync(['mission', '--plan', 'Plan Mission Alpha'], {
      from: 'user',
    });

    const files = await readdir(path.join(projectDir, 'docs', 'prdy'));
    const raw = await readFile(path.join(projectDir, 'docs', 'prdy', files[0]!), 'utf8');
    const persisted = parseYaml(raw) as { missions: Array<Record<string, unknown>> };

    expect(persisted.missions).toHaveLength(1);
    expect(persisted.missions[0]).toMatchObject({ missionId: 'mission-plan-1' });
  });

  it('the mission path and the prdy path resolve to the same store with stable ids/versions', async () => {
    // Mission path.
    await buildTestProgram().parseAsync(['mission', '--plan', 'Plan Mission Alpha'], {
      from: 'user',
    });

    // prdy path (service, non-interactive entry).
    const service = new PrdService({ projectPath: projectDir });
    const direct = await service.create({ name: 'Directly Created' });

    const all = await service.list();
    expect(all.map((doc) => doc.id).sort()).toEqual([...all.map((doc) => doc.id)].sort());
    expect(all).toHaveLength(2);

    const files = await readdir(path.join(projectDir, 'docs', 'prdy'));
    expect(files).toContain(`${direct.id}.yaml`);

    // Both are v1 in the same store with distinct stable ids.
    for (const doc of all) {
      expect(doc.version).toBe(1);
      expect(files).toContain(`${doc.id}.yaml`);
    }
  });
});
