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

// --- I/O mocks --------------------------------------------------------------
const execFileMock = vi.hoisted(() => vi.fn());
vi.mock('child_process', () => ({ execFile: execFileMock }));

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

type Handler = (command: string, args: string[]) => { error?: NodeJS.ErrnoException; stdout?: string };

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 ?? '' });
    },
  );
}

const { getHermesTelemetrySnapshot, getHermesTokenUsageSnapshot, clearHermesTelemetryCache } = await import('./repository.js');

describe('hermes-telemetry repository', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    clearHermesTelemetryCache();
  });

  it('returns a Zod-valid snapshot when every probe fails (CLI missing)', async () => {
    // ENOENT on every shell-out (no `hermes` CLI available).
    setExec(() => {
      const err = Object.assign(new Error('ENOENT'), { code: 'ENOENT' as const });
      return { error: err };
    });
    statMock.mockRejectedValue(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));
    readFileMock.mockRejectedValue(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));
    readdirMock.mockRejectedValue(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));

    const snapshot = await getHermesTelemetrySnapshot('vijay');
    // The whole shape must validate even when nothing was readable — that's
    // the contract the route handler relies on to send a stable JSON to the
    // UI rather than a 500.
    expect(() => HermesTelemetrySnapshotSchema.parse(snapshot)).not.toThrow();
    expect(snapshot.instanceId).toBe('vijay');
    expect(snapshot.sessions.status).toBe('unknown');
    expect(snapshot.cron.status).toBe('unknown');
    expect(snapshot.memory.status).toBe('unknown');
    expect(snapshot.skills.status).toBe('unknown');
    expect(snapshot.watchdog.status).toBe('unknown');
    expect(snapshot.backupHistory.status).toBe('unknown');
    expect(snapshot.warnings.length).toBeGreaterThan(0);
  });

  it('parses sessions/cron/memory/skills JSON output when the CLI is present', async () => {
    setExec((command, args) => {
      if (command === 'hermes' && args.slice(0, 2).join(' ') === 'sessions stats') {
        return { stdout: JSON.stringify({ sessions: 59, messages: 5225 }) };
      }
      if (command === 'hermes' && args.slice(0, 2).join(' ') === 'cron list') {
        return {
          stdout: JSON.stringify([
            { id: 'mem-rotate', name: 'Memory rotation', schedule: '0 4 * * *', last_run: '2026-01-01T04:00:00Z', next_run: '2026-01-02T04:00:00Z', last_status: 'ok', active: true },
          ]),
        };
      }
      if (command === 'hermes' && args.slice(0, 2).join(' ') === 'memory list') {
        return { stdout: JSON.stringify([{ id: 'm1', type: 'note', key: 'gateway', summary: 'restart procedure' }]) };
      }
      if (command === 'hermes' && args.slice(0, 2).join(' ') === 'skills list') {
        return { stdout: JSON.stringify([{ id: 's1', name: 'restart', description: 'restart a service', enabled: true }]) };
      }
      if (command === 'git') {
        // No backup repo on the test box.
        const err = Object.assign(new Error('ENOENT'), { code: 'ENOENT' as const });
        return { error: err };
      }
      return { stdout: '' };
    });
    statMock.mockRejectedValue(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));
    readdirMock.mockResolvedValue([]);

    const snapshot = await getHermesTelemetrySnapshot('vijay');
    expect(snapshot.sessions).toEqual({ totalSessions: 59, totalMessages: 5225, status: 'up' });
    expect(snapshot.cron.status).toBe('up');
    expect(snapshot.cron.entries).toHaveLength(1);
    expect(snapshot.cron.entries[0].name).toBe('Memory rotation');
    expect(snapshot.memory.status).toBe('up');
    expect(snapshot.memory.items[0].key).toBe('gateway');
    expect(snapshot.skills.status).toBe('up');
    expect(snapshot.skills.items[0].id).toBe('s1');
  });

  it('parses watchdog log lines into severity-tagged alerts', async () => {
    setExec(() => {
      const err = Object.assign(new Error('ENOENT'), { code: 'ENOENT' as const });
      return { error: err };
    });
    statMock.mockResolvedValue({} as never);
    readdirMock.mockResolvedValue([]);
    readFileMock.mockResolvedValue([
      '2026-01-01T12:34:56 WARNING gateway is degraded',
      '2026-01-01T12:35:01 CRITICAL backup repo HEAD missing',
      '2026-01-01T12:35:30 INFO healthy after retry',
      '',
    ].join('\n'));

    const snapshot = await getHermesTelemetrySnapshot('bheem');
    expect(snapshot.watchdog.status).toBe('up');
    expect(snapshot.watchdog.alerts.map((a) => a.severity)).toEqual(['warn', 'critical', 'info']);
    expect(snapshot.watchdog.alerts[0].message).toBe('gateway is degraded');
  });

  it('parses backup history from `git log` output', async () => {
    setExec((command, args) => {
      if (command === 'git' && args.includes('log')) {
        return {
          stdout: [
            'a1b2c3\x1f2026-01-01T01:00:00Z\xfeBackup at 01:00',
            'd4e5f6\x1f2026-01-01T00:00:00Z\xfeBackup at 00:00',
          ].map((s) => s.replace('\xfe', '\x1f')).join('\n'),
        };
      }
      const err = Object.assign(new Error('ENOENT'), { code: 'ENOENT' as const });
      return { error: err };
    });
    statMock.mockRejectedValue(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));
    readdirMock.mockResolvedValue([]);

    const snapshot = await getHermesTelemetrySnapshot('vijay');
    expect(snapshot.backupHistory.status).toBe('up');
    expect(snapshot.backupHistory.entries).toHaveLength(2);
    expect(snapshot.backupHistory.entries[0]).toMatchObject({ sha: 'a1b2c3', subject: 'Backup at 01:00' });
  });

  it('serves a cached snapshot on the second call within the TTL window', async () => {
    let calls = 0;
    setExec(() => {
      calls++;
      const err = Object.assign(new Error('ENOENT'), { code: 'ENOENT' as const });
      return { error: err };
    });
    statMock.mockRejectedValue(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));
    readdirMock.mockRejectedValue(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));

    const a = await getHermesTelemetrySnapshot('vijay');
    const callsAfterFirst = calls;
    const b = await getHermesTelemetrySnapshot('vijay');
    expect(calls).toBe(callsAfterFirst); // no extra subprocess work
    expect(b.cached).toBe(true);
    expect(a.instanceId).toBe(b.instanceId);
  });

  it('caches per instance independently', async () => {
    setExec(() => {
      const err = Object.assign(new Error('ENOENT'), { code: 'ENOENT' as const });
      return { error: err };
    });
    statMock.mockRejectedValue(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));
    readdirMock.mockRejectedValue(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));

    const v = await getHermesTelemetrySnapshot('vijay');
    const b = await getHermesTelemetrySnapshot('bheem');
    expect(v.instanceId).toBe('vijay');
    expect(b.instanceId).toBe('bheem');
  });

  it('parses sanitized Hermes session JSONL events without exposing raw message content', async () => {
    setExec(() => {
      const err = Object.assign(new Error('ENOENT'), { code: 'ENOENT' as const });
      return { error: err };
    });
    statMock.mockRejectedValue(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }));
    readdirMock.mockResolvedValue(['20260101_session.jsonl']);
    readFileMock.mockImplementation(async (path: string) => {
      if (path.endsWith('.jsonl')) {
        return [
          JSON.stringify({ role: 'user', content: 'secret prompt', timestamp: '2026-01-01T00:00:00Z' }),
          JSON.stringify({ role: 'assistant', finish_reason: 'tool_calls', tool_calls: [{ function: { name: 'exec_command' } }], timestamp: '2026-01-01T00:01:00Z' }),
        ].join('\n');
      }
      throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
    });

    const snapshot = await getHermesTelemetrySnapshot('vijay');
    expect(snapshot.sessionEvents.status).toBe('up');
    expect(snapshot.sessionEvents.sourceCount).toBe(1);
    expect(snapshot.sessionEvents.entries).toHaveLength(2);
    expect(snapshot.sessionEvents.entries[0].summary).toBe('assistant tool call: exec_command');
    expect(snapshot.sessionEvents.entries[1].summary).toBe('user message (content redacted)');
    expect(JSON.stringify(snapshot.sessionEvents.entries)).not.toContain('secret prompt');
  });

  it('reads token usage analytics from the Hermes state database without message content', async () => {
    setExec((command, args) => {
      if (command === 'python3' && args[0] === '-c') {
        return {
          stdout: JSON.stringify({
            summary: {
              sessionCount: 3,
              messageCount: 42,
              toolCallCount: 7,
              inputTokens: 1000,
              outputTokens: 250,
              cacheReadTokens: 5000,
              cacheWriteTokens: 50,
              reasoningTokens: 25,
              totalTokens: 6325,
              estimatedCostUsd: 1.23,
              actualCostUsd: 0.75,
              firstStartedAt: '2026-06-01T00:00:00.000Z',
              lastActivityAt: '2026-06-06T00:00:00.000Z',
            },
            byModel: [{ model: 'gpt-5.5', provider: 'openai-codex', sessionCount: 2, totalTokens: 6000, estimatedCostUsd: 1.2 }],
            byDay: [{ day: '2026-06-06', sessionCount: 1, totalTokens: 4000, estimatedCostUsd: 0.8 }],
            recentSessions: [{ id: 's1', title: 'DevOps task', source: 'telegram', model: 'gpt-5.5', provider: 'openai-codex', startedAt: '2026-06-06T00:00:00.000Z', endedAt: null, totalTokens: 4000, messageCount: 12, toolCallCount: 3, estimatedCostUsd: 0.8 }],
          }),
        };
      }
      return { stdout: '' };
    });

    const snapshot = await getHermesTokenUsageSnapshot('vijay');
    expect(snapshot.instanceId).toBe('vijay');
    expect(snapshot.status).toBe('up');
    expect(snapshot.summary.totalTokens).toBe(6325);
    expect(snapshot.byModel[0]).toMatchObject({ model: 'gpt-5.5', provider: 'openai-codex', totalTokens: 6000 });
    expect(snapshot.byDay[0]).toMatchObject({ day: '2026-06-06', sessionCount: 1 });
    expect(snapshot.recentSessions[0]).toMatchObject({ id: 's1', title: 'DevOps task', totalTokens: 4000 });
    expect(JSON.stringify(snapshot)).not.toContain('secret prompt');
  });
});
