/**
 * Tests for the heartbeat_max_age health-check type (#2787) and the
 * output_paths recipe frontmatter + configured-collector helper (#2788).
 * Heartbeat files live under a temp GBRAIN_HOME so nothing touches ~/.gbrain.
 */

import { describe, test, expect } from 'bun:test';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { withEnv } from './helpers/with-env.ts';
import {
  parseMaxAge,
  executeHealthCheck,
  parseRecipe,
  getConfiguredCollectorOutputs,
} from '../src/commands/integrations.ts';

function tempHome(): string {
  return mkdtempSync(join(tmpdir(), 'gbrain-hb-'));
}

function writeHeartbeat(home: string, id: string, entries: Array<{ ts: string; event: string; status: string }>): void {
  const dir = join(home, '.gbrain', 'integrations', id);
  mkdirSync(dir, { recursive: true });
  writeFileSync(join(dir, 'heartbeat.jsonl'), entries.map(e => JSON.stringify(e)).join('\n') + '\n');
}

describe('parseMaxAge', () => {
  test('parses h/d/m/s durations', () => {
    expect(parseMaxAge('48h')).toBe(48 * 3_600_000);
    expect(parseMaxAge('2d')).toBe(2 * 86_400_000);
    expect(parseMaxAge('90m')).toBe(90 * 60_000);
    expect(parseMaxAge('30s')).toBe(30_000);
    expect(parseMaxAge(' 48H ')).toBe(48 * 3_600_000);
  });

  test('rejects garbage', () => {
    expect(parseMaxAge('abc')).toBeNull();
    expect(parseMaxAge('48')).toBeNull();
    expect(parseMaxAge('-3h')).toBeNull();
    expect(parseMaxAge('')).toBeNull();
    expect(parseMaxAge('0h')).toBeNull();
  });
});

describe('heartbeat_max_age health check (#2787)', () => {
  test('fresh heartbeat within max_age → ok', async () => {
    const home = tempHome();
    try {
      writeHeartbeat(home, 'calendar-to-brain', [
        { ts: new Date(Date.now() - 3_600_000).toISOString(), event: 'sync', status: 'ok' },
      ]);
      await withEnv({ GBRAIN_HOME: home }, async () => {
        const r = await executeHealthCheck(
          { type: 'heartbeat_max_age', max_age: '48h', label: 'freshness' } as any,
          'calendar-to-brain',
          true,
        );
        expect(r.status).toBe('ok');
        expect(r.output).toContain('within 48h');
      });
    } finally {
      rmSync(home, { recursive: true, force: true });
    }
  });

  test('16-day-stale sense FAILS (the #2787 silent-death receipt)', async () => {
    const home = tempHome();
    try {
      writeHeartbeat(home, 'calendar-to-brain', [
        { ts: new Date(Date.now() - 16 * 86_400_000).toISOString(), event: 'sync', status: 'ok' },
      ]);
      await withEnv({ GBRAIN_HOME: home }, async () => {
        const r = await executeHealthCheck(
          { type: 'heartbeat_max_age', max_age: '48h' } as any,
          'calendar-to-brain',
          true,
        );
        expect(r.status).toBe('fail');
        expect(r.output).toContain('exceeds max_age 48h');
        expect(r.output).toContain('stopped producing data');
      });
    } finally {
      rmSync(home, { recursive: true, force: true });
    }
  });

  test('no heartbeat data at all → fail', async () => {
    const home = tempHome();
    try {
      await withEnv({ GBRAIN_HOME: home }, async () => {
        const r = await executeHealthCheck(
          { type: 'heartbeat_max_age', max_age: '48h' } as any,
          'never-ran',
          true,
        );
        expect(r.status).toBe('fail');
        expect(r.output).toContain('no heartbeat events');
      });
    } finally {
      rmSync(home, { recursive: true, force: true });
    }
  });

  test('invalid max_age → fail with guidance, not a crash', async () => {
    const r = await executeHealthCheck(
      { type: 'heartbeat_max_age', max_age: 'soon' } as any,
      'whatever',
      true,
    );
    expect(r.status).toBe('fail');
    expect(r.output).toContain("invalid max_age 'soon'");
  });

  test('not gated on embedded trust (read-only local file)', async () => {
    const home = tempHome();
    try {
      writeHeartbeat(home, 'user-recipe', [
        { ts: new Date().toISOString(), event: 'sync', status: 'ok' },
      ]);
      await withEnv({ GBRAIN_HOME: home }, async () => {
        const r = await executeHealthCheck(
          { type: 'heartbeat_max_age', max_age: '1d' } as any,
          'user-recipe',
          false, // NOT embedded
        );
        expect(r.status).toBe('ok');
      });
    } finally {
      rmSync(home, { recursive: true, force: true });
    }
  });

  test('newest entry wins even when the file is not time-ordered', async () => {
    const home = tempHome();
    try {
      writeHeartbeat(home, 'unordered', [
        { ts: new Date(Date.now() - 60_000).toISOString(), event: 'sync', status: 'ok' },
        { ts: new Date(Date.now() - 20 * 86_400_000).toISOString(), event: 'sync', status: 'ok' },
      ]);
      await withEnv({ GBRAIN_HOME: home }, async () => {
        const r = await executeHealthCheck(
          { type: 'heartbeat_max_age', max_age: '48h' } as any,
          'unordered',
          true,
        );
        expect(r.status).toBe('ok');
      });
    } finally {
      rmSync(home, { recursive: true, force: true });
    }
  });

  test('works inside any_of', async () => {
    const home = tempHome();
    try {
      writeHeartbeat(home, 'combo', [
        { ts: new Date().toISOString(), event: 'sync', status: 'ok' },
      ]);
      await withEnv({ GBRAIN_HOME: home }, async () => {
        const r = await executeHealthCheck(
          { type: 'any_of', checks: [{ type: 'heartbeat_max_age', max_age: '1h' }] } as any,
          'combo',
          true,
        );
        expect(r.status).toBe('ok');
      });
    } finally {
      rmSync(home, { recursive: true, force: true });
    }
  });
});

describe('output_paths frontmatter + configured-collector outputs (#2788)', () => {
  const RECIPE = `---
id: test-collector
name: Test Collector
version: 0.1.0
description: writes files
category: sense
health_checks: []
output_paths:
  - daily/test-collector/
setup_time: 1 min
---
Body.
`;

  test('parseRecipe surfaces output_paths (and defaults to [])', () => {
    const parsed = parseRecipe(RECIPE, 'test-collector.md');
    expect(parsed).not.toBeNull();
    expect(parsed!.frontmatter.output_paths).toEqual(['daily/test-collector/']);
    const bare = parseRecipe('---\nid: bare\n---\nBody.', 'bare.md');
    expect(bare!.frontmatter.output_paths).toEqual([]);
  });

  test('the shipped calendar-to-brain recipe declares heartbeat_max_age + output_paths', () => {
    const content = require('node:fs').readFileSync(
      join(import.meta.dir, '..', 'recipes', 'calendar-to-brain.md'),
      'utf-8',
    );
    const parsed = parseRecipe(content, 'calendar-to-brain.md');
    expect(parsed).not.toBeNull();
    expect(parsed!.frontmatter.output_paths).toEqual(['daily/calendar/']);
    const hb = parsed!.frontmatter.health_checks.find(
      (c: any) => typeof c === 'object' && c.type === 'heartbeat_max_age',
    ) as any;
    expect(hb).toBeDefined();
    expect(hb.max_age).toBe('48h');
  });

  test('getConfiguredCollectorOutputs includes secretless recipes with output_paths', async () => {
    const home = tempHome();
    const recipesDir = mkdtempSync(join(tmpdir(), 'gbrain-recipes-'));
    try {
      writeFileSync(join(recipesDir, 'test-collector.md'), RECIPE);
      await withEnv({ GBRAIN_HOME: home, GBRAIN_RECIPES_DIR: recipesDir }, async () => {
        const outputs = getConfiguredCollectorOutputs();
        expect(outputs).toContainEqual({ id: 'test-collector', output_path: 'daily/test-collector/' });
      });
    } finally {
      rmSync(home, { recursive: true, force: true });
      rmSync(recipesDir, { recursive: true, force: true });
    }
  });
});
