import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import type { Service } from '../services/types.js';

const { checkServiceHealth, checkAllServices, clearHealthCache } = await import('./repository.js');

function makeService(overrides?: Partial<Service>): Service {
  return {
    id: 'svc-1',
    name: 'Test Service',
    scriptPath: '../deploy.sh',
    healthUrl: 'https://example.com/health',
    repoPath: '../repo',
    status: 'up',
    version: '1.0.0',
    productId: 'devops-internal',
    ...overrides,
  };
}

describe('checkServiceHealth', () => {
  beforeEach(() => {
    clearHealthCache();
    vi.useFakeTimers();
    vi.setSystemTime(new Date('2026-01-01T00:00:00Z'));
    // Each test installs its own fetch mock as needed.
    vi.stubGlobal('fetch', vi.fn());
  });

  afterEach(() => {
    vi.useRealTimers();
    vi.unstubAllGlobals();
  });

  it('reports "up" for a fast 2xx response', async () => {
    (globalThis.fetch as unknown as ReturnType<typeof vi.fn>).mockResolvedValue({ ok: true });
    const result = await checkServiceHealth(makeService());
    expect(result.status).toBe('up');
    expect(result.serviceId).toBe('svc-1');
    expect(result.lastCheck).toBe('2026-01-01T00:00:00.000Z');
    expect(result.responseTime).toBeGreaterThanOrEqual(0);
  });

  it('reports "down" for a non-2xx response', async () => {
    (globalThis.fetch as unknown as ReturnType<typeof vi.fn>).mockResolvedValue({ ok: false });
    const result = await checkServiceHealth(makeService({ id: 'svc-down' }));
    expect(result.status).toBe('down');
  });

  it('reports "down" when fetch throws (network/timeout)', async () => {
    (globalThis.fetch as unknown as ReturnType<typeof vi.fn>).mockRejectedValue(new Error('boom'));
    const result = await checkServiceHealth(makeService({ id: 'svc-net' }));
    expect(result.status).toBe('down');
    // Failure path does not record a responseTime.
    expect(result.responseTime).toBeUndefined();
  });

  it('caches successful results within the 30s TTL window', async () => {
    const fetchMock = globalThis.fetch as unknown as ReturnType<typeof vi.fn>;
    fetchMock.mockResolvedValue({ ok: true });
    await checkServiceHealth(makeService({ id: 'svc-cache' }));
    await checkServiceHealth(makeService({ id: 'svc-cache' }));
    await checkServiceHealth(makeService({ id: 'svc-cache' }));
    expect(fetchMock).toHaveBeenCalledTimes(1);
  });

  it('refetches after the cache TTL expires', async () => {
    const fetchMock = globalThis.fetch as unknown as ReturnType<typeof vi.fn>;
    fetchMock.mockResolvedValue({ ok: true });
    await checkServiceHealth(makeService({ id: 'svc-ttl' }));
    vi.setSystemTime(new Date(Date.now() + 31_000));
    await checkServiceHealth(makeService({ id: 'svc-ttl' }));
    expect(fetchMock).toHaveBeenCalledTimes(2);
  });

  it('caches failures for ~5s, not the full 30s', async () => {
    const fetchMock = globalThis.fetch as unknown as ReturnType<typeof vi.fn>;
    fetchMock.mockRejectedValue(new Error('boom'));
    await checkServiceHealth(makeService({ id: 'svc-fail' }));
    expect(fetchMock).toHaveBeenCalledTimes(1);

    // Within the short failure-cache window: still served from cache.
    vi.setSystemTime(new Date(Date.now() + 4_000));
    await checkServiceHealth(makeService({ id: 'svc-fail' }));
    expect(fetchMock).toHaveBeenCalledTimes(1);

    // Past the short failure window: refetch.
    vi.setSystemTime(new Date(Date.now() + 2_000));
    await checkServiceHealth(makeService({ id: 'svc-fail' }));
    expect(fetchMock).toHaveBeenCalledTimes(2);
  });

  it('clearHealthCache forces a refetch', async () => {
    const fetchMock = globalThis.fetch as unknown as ReturnType<typeof vi.fn>;
    fetchMock.mockResolvedValue({ ok: true });
    await checkServiceHealth(makeService({ id: 'svc-clear' }));
    clearHealthCache();
    await checkServiceHealth(makeService({ id: 'svc-clear' }));
    expect(fetchMock).toHaveBeenCalledTimes(2);
  });
});

describe('checkAllServices', () => {
  beforeEach(() => {
    clearHealthCache();
    vi.stubGlobal('fetch', vi.fn());
  });

  afterEach(() => {
    vi.unstubAllGlobals();
  });

  it('returns a result per input service in input order', async () => {
    const fetchMock = globalThis.fetch as unknown as ReturnType<typeof vi.fn>;
    fetchMock.mockImplementation(async (url: string) => ({ ok: !url.includes('bad') }));

    const services = [
      makeService({ id: 'a', healthUrl: 'https://a.example.com/health' }),
      makeService({ id: 'b', healthUrl: 'https://bad.example.com/health' }),
      makeService({ id: 'c', healthUrl: 'https://c.example.com/health' }),
    ];

    const out = await checkAllServices(services);
    expect(out).toHaveLength(3);
    expect(out.map(h => h.serviceId)).toEqual(['a', 'b', 'c']);
    expect(out.map(h => h.status)).toEqual(['up', 'down', 'up']);
  });
});
