import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import {
  buildReseedCommand,
  buildRelaunchCommands,
  readRosterAgentNames,
  runFrameworkReseed,
  refreshActiveFleetUnits,
  readInstalledFrameworkVersion,
  readBundledFrameworkVersion,
  checkFrameworkDrift,
} from './update-checker.js';
import { existsSync, readFileSync } from 'node:fs';

/**
 * F3-m3 / R13: `mosaic update` re-seeds the framework + (opt-in) relaunches
 * durable agents so shipped launcher/runtime changes activate. These cover the
 * pure builders + the missing-installer guard (the exec path is integration).
 */

describe('buildReseedCommand', () => {
  it('invokes the package install.sh in data-safe sync-only keep mode', () => {
    const out = buildReseedCommand('/pkg/framework', '/home/u/.config/mosaic');
    expect(out.installer).toBe('/pkg/framework/install.sh');
    expect(out.command).toBe('bash /pkg/framework/install.sh');
    expect(out.env).toEqual({
      MOSAIC_SYNC_ONLY: '1',
      MOSAIC_INSTALL_MODE: 'keep',
      MOSAIC_HOME: '/home/u/.config/mosaic',
    });
  });
});

describe('buildRelaunchCommands', () => {
  it('builds a systemctl --user restart per agent unit', () => {
    expect(buildRelaunchCommands(['orchestrator', 'coder0'])).toEqual([
      ['systemctl', '--user', 'restart', 'mosaic-agent@orchestrator.service'],
      ['systemctl', '--user', 'restart', 'mosaic-agent@coder0.service'],
    ]);
  });

  it('is empty for an empty roster', () => {
    expect(buildRelaunchCommands([])).toEqual([]);
  });
});

describe('readRosterAgentNames', () => {
  let home: string;

  beforeEach(() => {
    home = mkdtempSync(join(tmpdir(), 'mosaic-roster-'));
  });
  afterEach(() => {
    rmSync(home, { recursive: true, force: true });
  });

  it('returns [] when no roster exists', () => {
    expect(readRosterAgentNames(home)).toEqual([]);
  });

  it('extracts agent names from roster.yaml', () => {
    mkdirSync(join(home, 'fleet'), { recursive: true });
    writeFileSync(
      join(home, 'fleet', 'roster.yaml'),
      [
        'version: 1',
        'agents:',
        '  - name: orchestrator',
        '    runtime: pi',
        '  - name: coder0',
        '    runtime: claude',
        '  - name: "reviewer-1"',
        '    runtime: codex',
      ].join('\n') + '\n',
    );
    expect(readRosterAgentNames(home)).toEqual(['orchestrator', 'coder0', 'reviewer-1']);
  });
});

describe('runFrameworkReseed', () => {
  it('reports not-ok (not throw) when the installer is absent', () => {
    const missing = mkdtempSync(join(tmpdir(), 'mosaic-noinstaller-'));
    const res = runFrameworkReseed(missing, join(missing, 'home'));
    expect(res.ok).toBe(false);
    expect(res.reason).toContain('installer not found');
    rmSync(missing, { recursive: true, force: true });
  });
});

describe('refreshActiveFleetUnits', () => {
  let root: string;
  let mosaicHome: string;
  let configHome: string;

  beforeEach(() => {
    root = mkdtempSync(join(tmpdir(), 'mosaic-units-'));
    mosaicHome = join(root, 'mosaic');
    configHome = join(root, 'config');
    mkdirSync(join(mosaicHome, 'systemd', 'user'), { recursive: true });
    mkdirSync(join(configHome, 'systemd', 'user'), { recursive: true });
    // Freshly re-seeded units (new content).
    writeFileSync(join(mosaicHome, 'systemd', 'user', 'mosaic-agent@.service'), 'NEW\n');
    writeFileSync(join(mosaicHome, 'systemd', 'user', 'mosaic-tmux-holder.service'), 'NEW\n');
  });
  afterEach(() => rmSync(root, { recursive: true, force: true }));

  it('refreshes active units when a fleet is already installed', () => {
    // Active dir already carries mosaic units (stale) → fleet is installed.
    writeFileSync(join(configHome, 'systemd', 'user', 'mosaic-agent@.service'), 'OLD\n');
    const res = refreshActiveFleetUnits(mosaicHome, {
      XDG_CONFIG_HOME: configHome,
    } as NodeJS.ProcessEnv);
    expect(res.refreshed).toContain('mosaic-agent@.service');
    expect(
      readFileSync(join(configHome, 'systemd', 'user', 'mosaic-agent@.service'), 'utf-8'),
    ).toBe('NEW\n');
  });

  it('is a no-op when no fleet is installed (active dir has no mosaic units)', () => {
    const res = refreshActiveFleetUnits(mosaicHome, {
      XDG_CONFIG_HOME: configHome,
    } as NodeJS.ProcessEnv);
    expect(res.refreshed).toEqual([]);
    expect(existsSync(join(configHome, 'systemd', 'user', 'mosaic-agent@.service'))).toBe(false);
  });
});

/**
 * #642: re-seed when the on-disk framework is older than the bundled one even
 * if no package is reported outdated (CLI upgraded outside `mosaic update`).
 */
describe('framework drift detection', () => {
  let home: string; // stand-in for ~/.config/mosaic
  let fw: string; // stand-in for the bundled framework root

  beforeEach(() => {
    const root = mkdtempSync(join(tmpdir(), 'mosaic-drift-'));
    home = join(root, 'mosaic');
    fw = join(root, 'framework');
    mkdirSync(home, { recursive: true });
    mkdirSync(fw, { recursive: true });
  });
  afterEach(() => {
    rmSync(join(home, '..'), { recursive: true, force: true });
  });

  const writeInstalled = (v: string) => writeFileSync(join(home, '.framework-version'), v);
  const writeBundled = (v: string) =>
    writeFileSync(join(fw, 'install.sh'), `#!/usr/bin/env bash\nFRAMEWORK_VERSION=${v}\n`);

  describe('readInstalledFrameworkVersion', () => {
    it('returns undefined when the version file is absent', () => {
      expect(readInstalledFrameworkVersion(home)).toBeUndefined();
    });
    it('parses the integer (tolerating surrounding whitespace)', () => {
      writeInstalled('  3\n');
      expect(readInstalledFrameworkVersion(home)).toBe(3);
    });
    it('returns undefined for non-numeric content', () => {
      writeInstalled('not-a-number\n');
      expect(readInstalledFrameworkVersion(home)).toBeUndefined();
    });
  });

  describe('readBundledFrameworkVersion', () => {
    it('returns undefined when install.sh is absent', () => {
      expect(readBundledFrameworkVersion(fw)).toBeUndefined();
    });
    it('parses FRAMEWORK_VERSION=<n> from install.sh', () => {
      writeBundled('4');
      expect(readBundledFrameworkVersion(fw)).toBe(4);
    });
  });

  describe('checkFrameworkDrift', () => {
    it('reports drift when on-disk is older than bundled', () => {
      writeInstalled('3');
      writeBundled('4');
      expect(checkFrameworkDrift(home, fw)).toEqual({ drifted: true, installed: 3, bundled: 4 });
    });
    it('no drift when versions match', () => {
      writeInstalled('4');
      writeBundled('4');
      expect(checkFrameworkDrift(home, fw)).toMatchObject({ drifted: false });
    });
    it('no drift when on-disk is newer than bundled', () => {
      writeInstalled('5');
      writeBundled('4');
      expect(checkFrameworkDrift(home, fw)).toMatchObject({ drifted: false });
    });
    it('no drift (conservative) when a version cannot be read', () => {
      writeBundled('4'); // installed version file missing
      expect(checkFrameworkDrift(home, fw)).toMatchObject({ drifted: false, bundled: 4 });
    });
  });
});
