import { describe, it, expect, beforeEach, vi } from 'vitest';
import { HermesOpsSnapshotSchema } from './types.js';

// --- Mocks for all I/O the repository performs ---------------------------------
const execFileMock = vi.hoisted(() => vi.fn());
vi.mock('child_process', () => ({ execFile: execFileMock }));

const readFileMock = vi.hoisted(() => vi.fn());
const statMock = vi.hoisted(() => vi.fn());
vi.mock('fs/promises', () => ({ readFile: readFileMock, stat: statMock }));

const existsSyncMock = vi.hoisted(() => vi.fn());
vi.mock('fs', () => ({ existsSync: existsSyncMock }));

const { getHermesOpsSnapshot, clearHermesOpsCache } = await import('./repository.js');

type CmdResult = { stdout?: string; error?: Error & { code?: string | number; killed?: boolean; stdout?: string } };
type Handler = (command: string, args: string[]) => CmdResult;

// promisify(execFile) calls execFile(cmd, args, options, callback) and resolves
// with the callback's second arg, or rejects with the first.
function setExec(handler: Handler) {
  execFileMock.mockImplementation(
    (command: string, args: string[], _opts: unknown, cb: (err: unknown, result?: { stdout: string }) => void) => {
    const res = handler(command, args);
    if (res.error) cb(res.error);
    else cb(null, { stdout: res.stdout ?? '' });
  });
}

// A fully-healthy fleet: every probe succeeds and reports good state.
function healthyHandler(): Handler {
  return (command, args) => {
    if (command === 'systemctl') {
      if (args[0] === 'is-active') return { stdout: 'active\n' };
      if (args[0] === 'is-enabled') return { stdout: 'enabled\n' };
      if (args[0] === 'show') {
        return { stdout: 'NextElapseUSecRealtime=Sat 2026-05-31 02:00:00\nLastTriggerUSec=Fri 2026-05-30 02:00:00\n' };
      }
    }
    if (command === 'runuser') {
      // -u uma -- systemctl --user is-active|is-enabled ...
      if (args.includes('is-active')) return { stdout: 'active\n' };
      if (args.includes('is-enabled')) return { stdout: 'enabled\n' };
    }
    if (command === 'git') {
      if (args[0] === 'branch') return { stdout: 'main\n' };
      if (args[0] === 'status') return { stdout: '' };
      if (args[0] === 'rev-parse') return { stdout: 'abc1234\n' };
      if (args[0] === 'log') return { stdout: '2026-05-30T02:00:00+00:00\n' };
    }
    if (command === 'du') return { stdout: '12M\t.git\n' };
    if (command === 'tailscale') return { stdout: '100.87.53.10\n' };
    if (command === 'ps') return { stdout: '' };
    return { stdout: '' };
  };
}

function inactiveError(stdout: string): CmdResult {
  // systemctl is-active for an inactive unit: exit 3, prints "inactive".
  const error = Object.assign(new Error('exit 3'), { code: 3, stdout });
  return { error };
}

function enoentError(): CmdResult {
  return { error: Object.assign(new Error('not found'), { code: 'ENOENT' }) };
}

beforeEach(() => {
  vi.clearAllMocks();
  clearHermesOpsCache();
  // Default fs: manifests/jobs readable, google token present, emergency token present.
  readFileMock.mockImplementation(async (p: string) => {
    if (p.endsWith('MANIFEST.json')) return JSON.stringify({ files: [1, 2, 3] });
    if (p.endsWith('jobs.json')) return JSON.stringify({ jobs: [{ id: 'a' }, { id: 'b' }] });
    throw new Error('no such file');
  });
  statMock.mockResolvedValue({ isFile: () => true, size: 500 });
  existsSyncMock.mockReturnValue(true);
});

describe('hermes-ops repository', () => {
  it('produces a schema-valid, fully-healthy snapshot with no warnings', async () => {
    setExec(healthyHandler());
    const snapshot = await getHermesOpsSnapshot({ force: true });

    expect(() => HermesOpsSnapshotSchema.parse(snapshot)).not.toThrow();
    expect(snapshot.cached).toBe(false);
    expect(snapshot.instances).toHaveLength(2);
    for (const inst of snapshot.instances) {
      expect(inst.gateway.status).toBe('up');
      expect(inst.gateway.active).toBe(true);
      expect(inst.gateway.enabled).toBe(true);
      expect(inst.dashboard.status).toBe('up');
      expect(inst.backup.timer.status).toBe('up');
      expect(inst.backup.repo.status).toBe('up');
      expect(inst.backup.repo.clean).toBe(true);
    }
    expect(snapshot.warnings).toHaveLength(0);
  });

  it('maps a confirmed-inactive unit to status "down" with a warning', async () => {
    setExec((command, args) => {
      if (command === 'systemctl' && args[0] === 'is-active' && args[1] === 'hermes-gateway.service') {
        return inactiveError('inactive\n');
      }
      return healthyHandler()(command, args);
    });

    const snapshot = await getHermesOpsSnapshot({ force: true });
    const vijay = snapshot.instances.find((i) => i.id === 'vijay')!;
    expect(vijay.gateway.status).toBe('down');
    expect(vijay.gateway.active).toBe(false);
    expect(snapshot.warnings).toContain('Vijay / root gateway is not active');
  });

  it('maps an un-runnable probe to status "unknown" (not a false "down")', async () => {
    setExec((command, args) => {
      if (command === 'systemctl' && args[0] === 'is-active' && args[1] === 'hermes-root-dashboard.service') {
        return enoentError();
      }
      return healthyHandler()(command, args);
    });

    const snapshot = await getHermesOpsSnapshot({ force: true });
    const vijay = snapshot.instances.find((i) => i.id === 'vijay')!;
    expect(vijay.dashboard.status).toBe('unknown');
    expect(snapshot.warnings).toContain('Vijay / root private dashboard status could not be determined');
    expect(snapshot.warnings).not.toContain('Vijay / root private dashboard is not active');
  });

  it('uses runuser --user for Bheem and falls back to ps when it cannot run', async () => {
    setExec((command, args) => {
      // runuser probe unavailable in this environment.
      if (command === 'runuser') return enoentError();
      // Legacy ps fallback shows uma's gateway process running.
      if (command === 'ps' && args[0] === '-eo') {
        return { stdout: 'uma /usr/bin/python -m hermes_cli.main gateway\nroot /usr/sbin/sshd\n' };
      }
      return healthyHandler()(command, args);
    });

    const snapshot = await getHermesOpsSnapshot({ force: true });
    const bheem = snapshot.instances.find((i) => i.id === 'bheem')!;
    expect(bheem.gateway.active).toBe(true);
    expect(bheem.gateway.status).toBe('up');
  });

  it('prefers a sanitized per-instance ops export when one is present', async () => {
    setExec(healthyHandler());
    readFileMock.mockImplementation(async (p: string) => {
      if (p === '/home/uma/.hermes/ops-export.json') {
        return JSON.stringify({
          gateway: { active: false, enabled: true, status: 'down' },
          dashboard: { active: false, status: 'down' },
          backupTimer: {
            name: 'uma-hermes-backup.timer',
            active: false,
            status: 'down',
            nextRun: null,
            lastRun: null,
          },
          repo: {
            path: '/home/uma/repos/uma_hostinger_hermes_vm',
            branch: 'main',
            clean: true,
            head: 'export1',
            lastCommitAt: '2026-05-31T00:00:00Z',
            size: '1M',
            status: 'up',
          },
          restoredFileCount: 42,
          restoredCronJobs: 3,
          googleWorkspaceToken: true,
        });
      }
      if (p.endsWith('MANIFEST.json')) return JSON.stringify({ files: [1, 2, 3] });
      if (p.endsWith('jobs.json')) return JSON.stringify({ jobs: [{ id: 'a' }, { id: 'b' }] });
      throw new Error('no such file');
    });

    const snapshot = await getHermesOpsSnapshot({ force: true });
    const bheem = snapshot.instances.find((i) => i.id === 'bheem')!;
    expect(bheem.gateway.status).toBe('down');
    expect(bheem.dashboard.status).toBe('down');
    expect(bheem.backup.repo.head).toBe('export1');
    expect(bheem.backup.restoredFileCount).toBe(42);
    expect(bheem.backup.restoredCronJobs).toBe(3);
    expect(bheem.google.workspaceToken).toBe(true);
  });

  it('reports unknown repo status when git cannot be read', async () => {
    setExec((command, args) => {
      if (command === 'git') return enoentError();
      return healthyHandler()(command, args);
    });

    const snapshot = await getHermesOpsSnapshot({ force: true });
    const vijay = snapshot.instances.find((i) => i.id === 'vijay')!;
    expect(vijay.backup.repo.status).toBe('unknown');
    expect(vijay.backup.repo.head).toBeNull();
    expect(snapshot.warnings).toContain('Vijay / root backup repo HEAD could not be read');
  });

  it('serves a cached snapshot within the TTL without re-probing', async () => {
    setExec(healthyHandler());

    const first = await getHermesOpsSnapshot();
    const callsAfterFirst = execFileMock.mock.calls.length;
    expect(callsAfterFirst).toBeGreaterThan(0);
    expect(first.cached).toBe(false);

    const second = await getHermesOpsSnapshot();
    expect(second.cached).toBe(true);
    // No additional subprocesses were spawned for the cached read.
    expect(execFileMock.mock.calls.length).toBe(callsAfterFirst);

    // force: true bypasses the cache and re-probes.
    const third = await getHermesOpsSnapshot({ force: true });
    expect(third.cached).toBe(false);
    expect(execFileMock.mock.calls.length).toBeGreaterThan(callsAfterFirst);
  });

  it('coalesces concurrent requests onto one computation', async () => {
    setExec(healthyHandler());
    const [a, b] = await Promise.all([getHermesOpsSnapshot(), getHermesOpsSnapshot()]);
    expect(a.generatedAt).toBe(b.generatedAt);
  });
});
