import { describe, it, expect, afterEach, vi } from 'vitest';
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import {
  ENFORCEMENT_HOOK_MARKERS,
  FAIL_LOUD_MESSAGE,
  settingsHasEnforcementHooks,
} from '../commands/install-ordering-guard.js';
import {
  runUpdatePathSettingsGuard,
  runUpdateReseedFlow,
  type FrameworkReseedResult,
} from './update-checker.js';

/**
 * Red-first tests for issue #882 (b) — the `mosaic update --sync-only`
 * install-ordering-guard bypass (Mos-ruled "Option C").
 *
 * Root cause under test: `runFrameworkReseed()` runs the package's
 * install.sh with MOSAIC_SYNC_ONLY=1, which exits after the file-system
 * phase, BEFORE the "Post-install tasks" step that would otherwise run
 * `mosaic-link-runtime-assets` — the only place the #869 Point-1 C2
 * install-ordering guard evaluated whether the lease-enforcement hooks
 * (PreToolUse mutator-gate.py / Stop receipt-observer-client.py) may be
 * wired into `~/.claude/settings.json`. A plain `mosaic update` therefore
 * never re-evaluated that decision. These tests prove the post-reseed step
 * added to close that gap (`runUpdatePathSettingsGuard`, wired into the
 * `mosaic update` reseed flow via `runUpdateReseedFlow`) reuses the EXACT
 * C2 guard — no forked logic — and is skipped only when `--no-reseed` means
 * there was nothing to re-seed/re-link in the first place.
 *
 * All fixtures use temp directories — this suite never reads or writes the
 * real `~/.claude/settings.json` or `~/.config/mosaic`.
 */

const FIXTURE_SETTINGS = {
  model: 'opus',
  hooks: {
    PreToolUse: [
      {
        matcher: '.*',
        hooks: [
          {
            type: 'command',
            command: 'python3 ~/.config/mosaic/tools/lease-broker/mutator-gate.py --runtime claude',
            timeout: 3,
          },
        ],
      },
    ],
    Stop: [
      {
        hooks: [
          {
            type: 'command',
            command:
              'python3 ~/.config/mosaic/tools/lease-broker/receipt-observer-client.py --runtime claude',
            timeout: 3,
          },
        ],
      },
    ],
  },
};

function fixtureJson(): string {
  return JSON.stringify(FIXTURE_SETTINGS, null, 2) + '\n';
}

describe('runUpdatePathSettingsGuard', () => {
  let root: string;
  let mosaicHome: string;
  let claudeHome: string;

  afterEach(() => {
    if (root) rmSync(root, { recursive: true, force: true });
  });

  function makeTemplate(): void {
    root = mkdtempSync(join(tmpdir(), 'mosaic-update-settings-guard-'));
    mosaicHome = join(root, 'mosaic-home');
    claudeHome = join(root, 'claude-home');
    mkdirSync(join(mosaicHome, 'runtime', 'claude'), { recursive: true });
    writeFileSync(join(mosaicHome, 'runtime', 'claude', 'settings.json'), fixtureJson());
  }

  it('does not run when there is no settings.json template to re-link', () => {
    root = mkdtempSync(join(tmpdir(), 'mosaic-update-settings-guard-'));
    mosaicHome = join(root, 'mosaic-home');
    claudeHome = join(root, 'claude-home');
    // Deliberately no runtime/claude/settings.json under mosaicHome.

    const outcome = runUpdatePathSettingsGuard(mosaicHome, claudeHome);

    expect(outcome.ran).toBe(false);
    expect(outcome.result).toBeUndefined();
    expect(existsSync(join(claudeHome, 'settings.json'))).toBe(false);
  });

  it('activatable=false (default, no opt-out): strips enforcement hooks and fails loud, exactly as install-time', () => {
    makeTemplate();

    const outcome = runUpdatePathSettingsGuard(
      mosaicHome,
      claudeHome,
      {},
      { activatable: () => false },
    );

    expect(outcome.ran).toBe(true);
    expect(outcome.result?.exitCode).toBe(1);
    expect(outcome.result?.wired).toBe(false);
    expect(outcome.result?.logs).toHaveLength(1);
    expect(outcome.result?.logs[0]?.level).toBe('error');
    expect(outcome.result?.logs[0]?.message).toBe(FAIL_LOUD_MESSAGE);

    const written = JSON.parse(readFileSync(join(claudeHome, 'settings.json'), 'utf-8')) as Record<
      string,
      unknown
    >;
    expect(settingsHasEnforcementHooks(written)).toBe(false);
  });

  it('activatable=true: wires hooks normally, no strip, no logs', () => {
    makeTemplate();

    const outcome = runUpdatePathSettingsGuard(
      mosaicHome,
      claudeHome,
      {},
      { activatable: () => true },
    );

    expect(outcome.ran).toBe(true);
    expect(outcome.result?.exitCode).toBe(0);
    expect(outcome.result?.wired).toBe(true);
    expect(outcome.result?.logs).toHaveLength(0);

    const written = JSON.parse(readFileSync(join(claudeHome, 'settings.json'), 'utf-8')) as Record<
      string,
      unknown
    >;
    expect(settingsHasEnforcementHooks(written)).toBe(true);
    expect(written).toEqual(FIXTURE_SETTINGS);
  });

  it('activatable=false + --allow-inactive-enforcement: wires hooks anyway with a loud warning', () => {
    makeTemplate();

    const outcome = runUpdatePathSettingsGuard(
      mosaicHome,
      claudeHome,
      { allowInactiveEnforcement: true },
      { activatable: () => false },
    );

    expect(outcome.ran).toBe(true);
    expect(outcome.result?.exitCode).toBe(0);
    expect(outcome.result?.wired).toBe(true);
    expect(outcome.result?.logs).toHaveLength(1);
    expect(outcome.result?.logs[0]?.level).toBe('warn');
    expect(outcome.result?.logs[0]?.message).toMatch(/WITHOUT confirmed activation/);

    const written = JSON.parse(readFileSync(join(claudeHome, 'settings.json'), 'utf-8')) as Record<
      string,
      unknown
    >;
    expect(settingsHasEnforcementHooks(written)).toBe(true);
  });

  it('never touches the real home directory settings path used by this test file', () => {
    // Sanity guard for the suite itself.
    makeTemplate();
    expect(mosaicHome).toContain('mosaic-update-settings-guard-');
    expect(claudeHome).toContain('mosaic-update-settings-guard-');
  });
});

describe('runUpdateReseedFlow (the `mosaic update` post-reseed guard wiring, #882 (b))', () => {
  const okReseed: FrameworkReseedResult = { ok: true };

  it('--no-reseed: the reseed is never attempted and the settings guard is never invoked', () => {
    const doReseed = vi.fn(() => okReseed);
    const doGuard = vi.fn(() => ({ ran: true }));
    const doRefresh = vi.fn(() => ({ refreshed: [], ok: true }));
    const doReadRoster = vi.fn(() => []);
    const log = vi.fn();
    const warnLog = vi.fn();
    const errorLog = vi.fn();

    const result = runUpdateReseedFlow(
      'should never be printed',
      { reseed: false },
      {
        runFrameworkReseed: doReseed,
        runUpdatePathSettingsGuard: doGuard,
        refreshActiveFleetUnits: doRefresh,
        readRosterAgentNames: doReadRoster,
        log,
        warnLog,
        errorLog,
      },
    );

    expect(result.attempted).toBe(false);
    expect(doReseed).not.toHaveBeenCalled();
    expect(doGuard).not.toHaveBeenCalled();
    expect(log).not.toHaveBeenCalled();
    expect(errorLog).not.toHaveBeenCalled();
  });

  it('reseed ran + activatable=false: the guard fires (hooks stripped) and the fail-loud message is surfaced, not swallowed', () => {
    const doReseed = vi.fn(() => okReseed);
    const doGuard = vi.fn(() => ({
      ran: true,
      result: {
        json: '{}',
        wired: false,
        exitCode: 1 as const,
        logs: [{ level: 'error' as const, message: FAIL_LOUD_MESSAGE }],
        destWritten: true,
      },
    }));
    const doRefresh = vi.fn(() => ({ refreshed: [], ok: true }));
    const doReadRoster = vi.fn(() => []);
    const log = vi.fn();
    const warnLog = vi.fn();
    const errorLog = vi.fn();

    const result = runUpdateReseedFlow(
      'Re-seeding…',
      { reseed: true },
      {
        runFrameworkReseed: doReseed,
        runUpdatePathSettingsGuard: doGuard,
        refreshActiveFleetUnits: doRefresh,
        readRosterAgentNames: doReadRoster,
        log,
        warnLog,
        errorLog,
      },
    );

    expect(result.attempted).toBe(true);
    expect(doReseed).toHaveBeenCalledTimes(1);
    expect(doGuard).toHaveBeenCalledTimes(1);
    expect(result.settingsGuard?.result?.exitCode).toBe(1);
    // The guard's fail-loud message must reach the operator (stderr), never swallowed.
    expect(errorLog).toHaveBeenCalledWith(FAIL_LOUD_MESSAGE);
  });

  it('reseed ran + activatable=true: the guard wires hooks with no error output', () => {
    const doReseed = vi.fn(() => okReseed);
    const doGuard = vi.fn(() => ({
      ran: true,
      result: {
        json: '{}',
        wired: true,
        exitCode: 0 as const,
        logs: [],
        destWritten: true,
      },
    }));
    const doRefresh = vi.fn(() => ({ refreshed: [], ok: true }));
    const doReadRoster = vi.fn(() => []);
    const log = vi.fn();
    const warnLog = vi.fn();
    const errorLog = vi.fn();

    const result = runUpdateReseedFlow(
      'Re-seeding…',
      { reseed: true },
      {
        runFrameworkReseed: doReseed,
        runUpdatePathSettingsGuard: doGuard,
        refreshActiveFleetUnits: doRefresh,
        readRosterAgentNames: doReadRoster,
        log,
        warnLog,
        errorLog,
      },
    );

    expect(result.attempted).toBe(true);
    expect(result.settingsGuard?.result?.exitCode).toBe(0);
    expect(errorLog).not.toHaveBeenCalled();
  });

  it('threads --allow-inactive-enforcement through to the settings guard', () => {
    const doReseed = vi.fn(() => okReseed);
    const doGuard = vi.fn(() => ({
      ran: true,
      result: {
        json: '{}',
        wired: true,
        exitCode: 0 as const,
        logs: [{ level: 'warn' as const, message: 'opt-out warning' }],
        destWritten: true,
      },
    }));
    const doRefresh = vi.fn(() => ({ refreshed: [], ok: true }));
    const doReadRoster = vi.fn(() => []);
    const warnLog = vi.fn();

    runUpdateReseedFlow(
      'Re-seeding…',
      { reseed: true, allowInactiveEnforcement: true },
      {
        runFrameworkReseed: doReseed,
        runUpdatePathSettingsGuard: doGuard,
        refreshActiveFleetUnits: doRefresh,
        readRosterAgentNames: doReadRoster,
        log: vi.fn(),
        warnLog,
        errorLog: vi.fn(),
      },
    );

    expect(doGuard).toHaveBeenCalledWith(undefined, undefined, {
      allowInactiveEnforcement: true,
    });
    expect(warnLog).toHaveBeenCalledWith('opt-out warning');
  });

  it('reseed failure: the settings guard is not invoked (nothing was re-seeded to re-link)', () => {
    const doReseed = vi.fn(
      () => ({ ok: false, reason: 'installer not found' }) as FrameworkReseedResult,
    );
    const doGuard = vi.fn(() => ({ ran: true }));
    const doRefresh = vi.fn(() => ({ refreshed: [], ok: true }));
    const doReadRoster = vi.fn(() => []);
    const errorLog = vi.fn();

    const result = runUpdateReseedFlow(
      'Re-seeding…',
      { reseed: true },
      {
        runFrameworkReseed: doReseed,
        runUpdatePathSettingsGuard: doGuard,
        refreshActiveFleetUnits: doRefresh,
        readRosterAgentNames: doReadRoster,
        log: vi.fn(),
        warnLog: vi.fn(),
        errorLog,
      },
    );

    expect(result.attempted).toBe(true);
    expect(result.settingsGuard).toBeUndefined();
    expect(doGuard).not.toHaveBeenCalled();
    expect(errorLog).toHaveBeenCalledWith(expect.stringContaining('Framework re-seed skipped'));
  });

  it('end-to-end (real runUpdatePathSettingsGuard, real temp files): reseed ok + activatable=false strips hooks in the live settings.json path', () => {
    const root = mkdtempSync(join(tmpdir(), 'mosaic-update-reseed-flow-e2e-'));
    try {
      const mosaicHome = join(root, 'mosaic-home');
      const claudeHome = join(root, 'claude-home');
      mkdirSync(join(mosaicHome, 'runtime', 'claude'), { recursive: true });
      writeFileSync(join(mosaicHome, 'runtime', 'claude', 'settings.json'), fixtureJson());
      // Pre-existing (stale, install-time) settings.json still carrying the
      // enforcement hooks — this is the exact state #882 (b) left behind.
      mkdirSync(claudeHome, { recursive: true });
      writeFileSync(join(claudeHome, 'settings.json'), fixtureJson());

      const errorLog = vi.fn();
      const result = runUpdateReseedFlow(
        'Re-seeding…',
        { reseed: true },
        {
          runFrameworkReseed: () => okReseed,
          runUpdatePathSettingsGuard: (mh, ch, options, deps) =>
            // Exercise the REAL function (imported above), pointed at temp dirs,
            // with the activation probe faked to prove this is not a live-host test.
            runUpdatePathSettingsGuardWithFakeActivation(
              mh ?? mosaicHome,
              ch ?? claudeHome,
              options,
              deps,
            ),
          refreshActiveFleetUnits: () => ({ refreshed: [], ok: true }),
          readRosterAgentNames: () => [],
          log: vi.fn(),
          warnLog: vi.fn(),
          errorLog,
        },
      );

      expect(result.settingsGuard?.result?.exitCode).toBe(1);
      const written = JSON.parse(
        readFileSync(join(claudeHome, 'settings.json'), 'utf-8'),
      ) as Record<string, unknown>;
      expect(settingsHasEnforcementHooks(written)).toBe(false);
      expect(errorLog).toHaveBeenCalledWith(FAIL_LOUD_MESSAGE);
    } finally {
      rmSync(root, { recursive: true, force: true });
    }
  });
});

function runUpdatePathSettingsGuardWithFakeActivation(
  mosaicHome: string,
  claudeHome: string,
  options: Parameters<typeof runUpdatePathSettingsGuard>[2],
  _deps: Parameters<typeof runUpdatePathSettingsGuard>[3],
): ReturnType<typeof runUpdatePathSettingsGuard> {
  return runUpdatePathSettingsGuard(mosaicHome, claudeHome, options, { activatable: () => false });
}

/**
 * Sanity check: the enforcement markers this suite exercises must match the
 * ones the C2 guard (`install-ordering-guard.ts`) actually looks for, so a
 * drift in either module's marker strings would fail this suite loudly
 * rather than silently passing on the wrong hooks.
 */
describe('marker parity with the C2 guard', () => {
  it('the fixture uses the same marker commands the guard matches on', () => {
    const preToolUse = FIXTURE_SETTINGS.hooks.PreToolUse[0]?.hooks[0]?.command ?? '';
    const stop = FIXTURE_SETTINGS.hooks.Stop[0]?.hooks[0]?.command ?? '';
    expect(preToolUse).toContain(ENFORCEMENT_HOOK_MARKERS.preToolUse);
    expect(stop).toContain(ENFORCEMENT_HOOK_MARKERS.stop);
  });
});
