import { access, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
import { constants } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { loadFleetRoster } from './fleet.js';
import { generateRoster, runProvision } from './fleet-provision.js';
import { loadProfile } from './fleet-profiles.js';

// These are INTEGRATION tests: each exercises real filesystem I/O — scanning the
// committed framework/fleet persona library, rendering YAML, writing to a temp
// mosaicHome, and round-tripping through the real roster parser. On a heavily
// contended CI runner (the whole monorepo's suites run in parallel) that genuine
// I/O can exceed vitest's 5s default even though it completes in ~400ms locally.
// Give the legitimately I/O-bound work generous headroom so CI is deterministic.
vi.setConfig({ testTimeout: 30_000 });

// The real, committed library: packages/mosaic/src/commands -> framework/fleet.
const frameworkFleet = resolve(
  dirname(fileURLToPath(import.meta.url)),
  '..',
  '..',
  'framework',
  'fleet',
);
const rolesDir = join(frameworkFleet, 'roles');
const profilesDir = join(frameworkFleet, 'profiles');

/** A fresh temp mosaicHome whose fleet/ dir is empty (for write-path tests). */
async function freshMosaicHome(): Promise<string> {
  const home = await mkdtemp(join(tmpdir(), 'mosaic-provision-'));
  await mkdir(join(home, 'fleet'), { recursive: true });
  return home;
}

async function fileExists(path: string): Promise<boolean> {
  try {
    await access(path, constants.F_OK);
    return true;
  } catch {
    return false;
  }
}

describe('provision software-delivery (floor, default)', () => {
  it('materializes only the floor seats with correct flags + valid scaffold', async () => {
    const profile = await loadProfile('software-delivery', { profilesDir, rolesDir });
    const { seats, yaml } = await generateRoster(profile, { profilesDir, rolesDir });

    // Floor is orchestrator + enhancer.
    expect(seats.map((s) => s.name).sort()).toEqual(['enhancer', 'orchestrator']);
    const orch = seats.find((s) => s.name === 'orchestrator');
    const enh = seats.find((s) => s.name === 'enhancer');
    // RULE 2: floor + lead get persistent_persona.
    expect(orch?.persistentPersona).toBe(true);
    expect(enh?.persistentPersona).toBe(true);
    // RULE 3: floor/lead do NOT reset between tasks.
    expect(orch?.resetBetweenTasks).toBeUndefined();
    expect(enh?.resetBetweenTasks).toBeUndefined();
    // RULE 4: default runtime claude.
    expect(orch?.runtime).toBe('claude');

    // Scaffold round-trips through the real parser.
    expect(yaml).toContain('version: 1');
    expect(yaml).toContain('transport: tmux');
    expect(yaml).toContain('socket_name: mosaic-fleet');
  });
});

describe('provision --full', () => {
  it('expands the entire roster, including multiplicity, deterministically', async () => {
    const profile = await loadProfile('software-delivery', { profilesDir, rolesDir });
    const { seats } = await generateRoster(profile, { full: true, profilesDir, rolesDir });

    const names = seats.map((s) => s.name);
    // code multiplicity 2 -> code0/code1 (RULE 1).
    expect(names).toContain('code0');
    expect(names).toContain('code1');
    expect(names).not.toContain('code');
    // Singleton seats keep the bare class name.
    expect(names).toContain('planner');
    expect(names).toContain('merge-gate');

    // Deterministic ordering: profile roster order, multiplicity expanded inline.
    const codeIdx0 = names.indexOf('code0');
    expect(names[codeIdx0 + 1]).toBe('code1');

    // RULE 3: a non-floor, non-lead execution seat resets between tasks.
    const code0 = seats.find((s) => s.name === 'code0');
    expect(code0?.resetBetweenTasks).toBe(true);
    expect(code0?.persistentPersona).toBeUndefined();

    // Seat count == sum of multiplicities.
    const expected = profile.roster.reduce((n, e) => n + e.multiplicity, 0);
    expect(seats.length).toBe(expected);
  });
});

describe('generated roster round-trips through the real parser', () => {
  it('feeds generated YAML back through loadFleetRoster (key correctness proof)', async () => {
    const home = await freshMosaicHome();
    try {
      const result = await runProvision('software-delivery', {
        mosaicHome: home,
        profilesDir,
        rolesDir,
        full: true,
        write: true,
      });
      expect(result.wrote).toBe(join(home, 'fleet', 'roster.yaml'));

      const parsed = await loadFleetRoster(result.wrote!);
      // It parses with no error and carries every seat.
      const profile = await loadProfile('software-delivery', { profilesDir, rolesDir });
      const expected = profile.roster.reduce((n, e) => n + e.multiplicity, 0);
      expect(parsed.agents.length).toBe(expected);
      // Classes survive the round-trip.
      expect(parsed.agents.some((a) => a.className === 'orchestrator')).toBe(true);
      expect(parsed.agents.filter((a) => a.className === 'code').length).toBe(2);
      // reports_to must NOT have been emitted (parser rejects unknown keys).
      expect(result.yaml).not.toContain('reports_to');
    } finally {
      await rm(home, { recursive: true, force: true });
    }
  });
});

describe('override-aware persona validation', () => {
  let dir: string;
  beforeEach(async () => {
    dir = await mkdtemp(join(tmpdir(), 'mosaic-provision-ov-'));
  });
  afterEach(async () => {
    await rm(dir, { recursive: true, force: true });
  });

  it('resolves a user-added (roles.local-only) persona without a false unresolved error', async () => {
    const overrideDir = join(dir, 'roles.local');
    const customProfilesDir = join(dir, 'profiles');
    await mkdir(overrideDir, { recursive: true });
    await mkdir(customProfilesDir, { recursive: true });
    // A brand-new class that exists ONLY in roles.local.
    await writeFile(
      join(overrideDir, 'widget-maker.md'),
      '# widget-maker\n\nThe widget-maker persona (`class: widget-maker`).\n',
    );
    await writeFile(
      join(customProfilesDir, 'custom.yaml'),
      [
        'id: custom',
        'title: Custom',
        'description: a custom system',
        'lead: widget-maker',
        'floor: [widget-maker]',
        'roster:',
        '  - class: widget-maker',
        '',
      ].join('\n'),
    );

    const result = await runProvision('custom', {
      mosaicHome: dir,
      profilesDir: customProfilesDir,
      // Point baseline rolesDir at a missing dir so resolution depends on override.
      rolesDir: join(dir, 'no-baseline'),
      overrideDir,
    });
    expect(result.yaml).toContain('class: widget-maker');
    // It resolved from the override layer.
    // (generateRoster records personaLayer; the seat is present.)
    expect(result.summary).toContain('persona=override');
  });

  it('FAILS with a clear message when a profile references a bogus class', async () => {
    const customProfilesDir = join(dir, 'profiles');
    await mkdir(customProfilesDir, { recursive: true });
    await writeFile(
      join(customProfilesDir, 'bogus.yaml'),
      [
        'id: bogus',
        'title: Bogus',
        'description: bad system',
        'lead: orchestrator',
        'floor: [orchestrator]',
        'roster:',
        '  - class: orchestrator',
        '  - class: not-a-real-persona-xyz',
        '    reports_to: orchestrator',
        '',
      ].join('\n'),
    );
    await expect(
      runProvision('bogus', {
        mosaicHome: dir,
        profilesDir: customProfilesDir,
        rolesDir,
        overrideDir: join(dir, 'roles.local'),
      }),
    ).rejects.toThrow(/not-a-real-persona-xyz|not a known persona class/);
  });
});

describe('--write protection', () => {
  it('refuses to clobber an existing roster.yaml without --force', async () => {
    const home = await freshMosaicHome();
    try {
      const rosterPath = join(home, 'fleet', 'roster.yaml');
      await writeFile(rosterPath, 'version: 1\n# operator customizations\n');
      await expect(
        runProvision('software-delivery', { mosaicHome: home, profilesDir, rolesDir, write: true }),
      ).rejects.toThrow(/Refusing to overwrite/);
      // The original file is untouched.
      const { readFile } = await import('node:fs/promises');
      expect(await readFile(rosterPath, 'utf8')).toContain('operator customizations');
    } finally {
      await rm(home, { recursive: true, force: true });
    }
  });

  it('--write --force overwrites an existing roster', async () => {
    const home = await freshMosaicHome();
    try {
      const rosterPath = join(home, 'fleet', 'roster.yaml');
      await writeFile(rosterPath, 'version: 1\n# old\n');
      const result = await runProvision('software-delivery', {
        mosaicHome: home,
        profilesDir,
        rolesDir,
        write: true,
        force: true,
      });
      expect(result.wrote).toBe(rosterPath);
      const parsed = await loadFleetRoster(rosterPath);
      expect(parsed.agents.length).toBeGreaterThan(0);
    } finally {
      await rm(home, { recursive: true, force: true });
    }
  });

  it('--write to a fresh mosaicHome creates the roster file', async () => {
    const home = await freshMosaicHome();
    try {
      const result = await runProvision('software-delivery', {
        mosaicHome: home,
        profilesDir,
        rolesDir,
        write: true,
      });
      expect(await fileExists(result.wrote!)).toBe(true);
    } finally {
      await rm(home, { recursive: true, force: true });
    }
  });

  it('dry run (no --write) writes nothing', async () => {
    const home = await freshMosaicHome();
    try {
      const result = await runProvision('software-delivery', {
        mosaicHome: home,
        profilesDir,
        rolesDir,
      });
      expect(result.wrote).toBeUndefined();
      expect(await fileExists(join(home, 'fleet', 'roster.yaml'))).toBe(false);
    } finally {
      await rm(home, { recursive: true, force: true });
    }
  });
});
