import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, readFileSync, existsSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { FileConfigAdapter, DEFAULT_SEED_FILES } from './file-adapter.js';

/**
 * Regression tests for the `FileConfigAdapter.syncFramework` seed behavior.
 *
 * Background: the bash installer (`framework/install.sh`) and this TS wizard
 * path both seed framework-contract files from `framework/defaults/` into the
 * user's mosaic home on first install. Before this fix:
 *
 *   - The bash installer only seeded `AGENTS.md` and `STANDARDS.md`, leaving
 *     `TOOLS.md` missing despite it being listed as mandatory in the
 *     AGENTS.md load order (position 5).
 *   - The TS wizard iterated every file in `defaults/` and copied it to the
 *     mosaic home root — including `defaults/SOUL.md` (hardcoded "Jarvis"),
 *     `defaults/USER.md` (placeholder), and internal framework files like
 *     `README.md` and `AUDIT-*.md`. That clobbered the identity flow on
 *     fresh installs and leaked framework-internal clutter into the user's
 *     home directory.
 *
 * This suite pins the whitelist and the preservation semantics so both
 * regressions stay fixed.
 */

function makeFixture(): { sourceDir: string; mosaicHome: string; defaultsDir: string } {
  const root = mkdtempSync(join(tmpdir(), 'mosaic-file-adapter-'));
  const sourceDir = join(root, 'source');
  const mosaicHome = join(root, 'mosaic-home');
  const defaultsDir = join(sourceDir, 'defaults');

  mkdirSync(defaultsDir, { recursive: true });
  mkdirSync(mosaicHome, { recursive: true });

  // Framework-contract defaults we expect the wizard to seed.
  writeFileSync(join(defaultsDir, 'CONSTITUTION.md'), '# CONSTITUTION default\n');
  writeFileSync(join(defaultsDir, 'AGENTS.md'), '# AGENTS default\n');
  writeFileSync(join(defaultsDir, 'STANDARDS.md'), '# STANDARDS default\n');
  writeFileSync(join(defaultsDir, 'TOOLS.md'), '# TOOLS default\n');

  // Non-contract files we must NOT seed on first install.
  writeFileSync(join(defaultsDir, 'SOUL.md'), '# SOUL default (should not be seeded)\n');
  writeFileSync(join(defaultsDir, 'USER.md'), '# USER default (should not be seeded)\n');
  writeFileSync(join(defaultsDir, 'README.md'), '# README (framework-internal)\n');
  writeFileSync(
    join(defaultsDir, 'AUDIT-2026-02-17-framework-consistency.md'),
    '# Audit snapshot\n',
  );

  return { sourceDir, mosaicHome, defaultsDir };
}

describe('FileConfigAdapter.syncFramework — defaults seeding', () => {
  let fixture: ReturnType<typeof makeFixture>;

  beforeEach(() => {
    fixture = makeFixture();
  });

  afterEach(() => {
    rmSync(join(fixture.sourceDir, '..'), { recursive: true, force: true });
  });

  it('seeds the four framework-contract files on a fresh mosaic home', async () => {
    const adapter = new FileConfigAdapter(fixture.mosaicHome, fixture.sourceDir);

    await adapter.syncFramework('fresh');

    for (const name of DEFAULT_SEED_FILES) {
      expect(existsSync(join(fixture.mosaicHome, name))).toBe(true);
    }
    expect(readFileSync(join(fixture.mosaicHome, 'TOOLS.md'), 'utf-8')).toContain(
      '# TOOLS default',
    );
  });

  it('does NOT seed SOUL.md or USER.md from defaults/ (wizard stages own those)', async () => {
    const adapter = new FileConfigAdapter(fixture.mosaicHome, fixture.sourceDir);

    await adapter.syncFramework('fresh');

    // SOUL.md and USER.md live in defaults/ for historical reasons, but they
    // are template-rendered per-user by the wizard stages. Seeding them here
    // would clobber the identity flow and leak placeholder content.
    expect(existsSync(join(fixture.mosaicHome, 'SOUL.md'))).toBe(false);
    expect(existsSync(join(fixture.mosaicHome, 'USER.md'))).toBe(false);
  });

  it('does NOT seed README.md or AUDIT-*.md from defaults/', async () => {
    const adapter = new FileConfigAdapter(fixture.mosaicHome, fixture.sourceDir);

    await adapter.syncFramework('fresh');

    expect(existsSync(join(fixture.mosaicHome, 'README.md'))).toBe(false);
    expect(existsSync(join(fixture.mosaicHome, 'AUDIT-2026-02-17-framework-consistency.md'))).toBe(
      false,
    );
  });

  it('overwrites framework-owned files (backup-once) but preserves user-seeded files', async () => {
    // Plant a root-level AGENTS.md in sourceDir so syncDirectory's preserve is exercised.
    writeFileSync(join(fixture.sourceDir, 'AGENTS.md'), '# shipped AGENTS from source root\n');

    writeFileSync(join(fixture.mosaicHome, 'TOOLS.md'), '# user-customized TOOLS\n');
    writeFileSync(join(fixture.mosaicHome, 'AGENTS.md'), '# user-customized AGENTS\n');

    const adapter = new FileConfigAdapter(fixture.mosaicHome, fixture.sourceDir);
    await adapter.syncFramework('keep');

    // User-seeded TOOLS.md is preserved.
    expect(readFileSync(join(fixture.mosaicHome, 'TOOLS.md'), 'utf-8')).toBe(
      '# user-customized TOOLS\n',
    );
    // Framework-owned AGENTS.md is overwritten from defaults/ ...
    expect(readFileSync(join(fixture.mosaicHome, 'AGENTS.md'), 'utf-8')).toBe('# AGENTS default\n');
    // ... and the user's prior copy is backed up exactly once.
    expect(readFileSync(join(fixture.mosaicHome, 'AGENTS.md.pre-constitution.bak'), 'utf-8')).toBe(
      '# user-customized AGENTS\n',
    );
    // Framework-owned STANDARDS.md (absent) gets installed.
    expect(readFileSync(join(fixture.mosaicHome, 'STANDARDS.md'), 'utf-8')).toContain(
      '# STANDARDS default',
    );
  });

  it('backs up a divergent framework-owned file only once (idempotent across re-sync)', async () => {
    writeFileSync(join(fixture.mosaicHome, 'AGENTS.md'), '# user-customized AGENTS\n');
    const adapter = new FileConfigAdapter(fixture.mosaicHome, fixture.sourceDir);

    await adapter.syncFramework('keep'); // 1st: backup created, AGENTS overwritten
    await adapter.syncFramework('keep'); // 2nd: AGENTS already == default, no new backup

    expect(readFileSync(join(fixture.mosaicHome, 'AGENTS.md.pre-constitution.bak'), 'utf-8')).toBe(
      '# user-customized AGENTS\n',
    );
  });

  it('preserves SOUL.md and credentials through a framework-owned overwrite', async () => {
    writeFileSync(join(fixture.mosaicHome, 'SOUL.md'), '# my persona\n');
    writeFileSync(join(fixture.mosaicHome, 'AGENTS.md'), '# user-customized AGENTS\n');
    mkdirSync(join(fixture.mosaicHome, 'credentials'), { recursive: true });
    writeFileSync(join(fixture.mosaicHome, 'credentials', 'c.json'), 'token\n');

    const adapter = new FileConfigAdapter(fixture.mosaicHome, fixture.sourceDir);
    await adapter.syncFramework('keep');

    expect(readFileSync(join(fixture.mosaicHome, 'SOUL.md'), 'utf-8')).toBe('# my persona\n');
    expect(readFileSync(join(fixture.mosaicHome, 'credentials', 'c.json'), 'utf-8')).toBe(
      'token\n',
    );
    expect(readFileSync(join(fixture.mosaicHome, 'AGENTS.md'), 'utf-8')).toBe('# AGENTS default\n');
  });

  it('preserves user fleet data (roster.yaml, agents/, run/) through a keep-mode sync', async () => {
    // Regression for the roster-loss bug (#631): user-authored fleet files must
    // survive the framework re-seed that `mosaic update` runs.
    mkdirSync(join(fixture.mosaicHome, 'fleet', 'run'), { recursive: true });
    mkdirSync(join(fixture.mosaicHome, 'fleet', 'agents'), { recursive: true });
    writeFileSync(join(fixture.mosaicHome, 'fleet', 'roster.yaml'), 'version: 1\nMINE\n');
    writeFileSync(join(fixture.mosaicHome, 'fleet', 'run', 'a.hb'), 'ts=x\n');
    writeFileSync(join(fixture.mosaicHome, 'fleet', 'agents', 'a.env'), 'X=1\n');
    // The framework ships fleet/examples — it should still seed/refresh.
    mkdirSync(join(fixture.sourceDir, 'fleet', 'examples'), { recursive: true });
    writeFileSync(join(fixture.sourceDir, 'fleet', 'examples', 'general.yaml'), '# preset\n');

    const adapter = new FileConfigAdapter(fixture.mosaicHome, fixture.sourceDir);
    await adapter.syncFramework('keep');

    expect(readFileSync(join(fixture.mosaicHome, 'fleet', 'roster.yaml'), 'utf-8')).toBe(
      'version: 1\nMINE\n',
    );
    expect(existsSync(join(fixture.mosaicHome, 'fleet', 'run', 'a.hb'))).toBe(true);
    expect(existsSync(join(fixture.mosaicHome, 'fleet', 'agents', 'a.env'))).toBe(true);
    // framework-owned fleet/examples is seeded
    expect(existsSync(join(fixture.mosaicHome, 'fleet', 'examples', 'general.yaml'))).toBe(true);
  });

  it('is a no-op for seeding when defaults/ dir does not exist', async () => {
    rmSync(fixture.defaultsDir, { recursive: true });

    const adapter = new FileConfigAdapter(fixture.mosaicHome, fixture.sourceDir);
    await expect(adapter.syncFramework('fresh')).resolves.toBeUndefined();

    expect(existsSync(join(fixture.mosaicHome, 'TOOLS.md'))).toBe(false);
  });
});
