/**
 * Unit tests for gateway-doctor.ts (mosaic gateway doctor).
 *
 * All external I/O is mocked — no live services required.
 */

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import type { TierHealthReport } from '@mosaicstack/storage';

/* ------------------------------------------------------------------ */
/*  Shared mock state                                                   */
/* ------------------------------------------------------------------ */

const mocks = vi.hoisted(() => {
  const mockLoadConfig = vi.fn();
  const mockProbeServiceHealth = vi.fn();
  const mockExistsSync = vi.fn();

  return { mockLoadConfig, mockProbeServiceHealth, mockExistsSync };
});

/* ------------------------------------------------------------------ */
/*  Module mocks                                                        */
/* ------------------------------------------------------------------ */

vi.mock('@mosaicstack/config', () => ({
  loadConfig: mocks.mockLoadConfig,
}));

vi.mock('@mosaicstack/storage', () => ({
  probeServiceHealth: mocks.mockProbeServiceHealth,
}));

vi.mock('node:fs', () => ({
  existsSync: mocks.mockExistsSync,
}));

/* ------------------------------------------------------------------ */
/*  Import SUT                                                          */
/* ------------------------------------------------------------------ */

import { runGatewayDoctor } from './gateway-doctor.js';
import type { MosaicConfig } from '@mosaicstack/config';

/* ------------------------------------------------------------------ */
/*  Fixtures                                                            */
/* ------------------------------------------------------------------ */

const STANDALONE_CONFIG: MosaicConfig = {
  tier: 'standalone',
  storage: { type: 'postgres', url: 'postgresql://mosaic:mosaic@localhost:5432/mosaic' },
  queue: { type: 'bullmq', url: 'redis://localhost:6380' },
  memory: { type: 'keyword' },
};

const GREEN_REPORT: TierHealthReport = {
  tier: 'standalone',
  configPath: '/some/mosaic.config.json',
  overall: 'green',
  services: [
    { name: 'postgres', status: 'ok', host: 'localhost', port: 5432, durationMs: 42 },
    { name: 'valkey', status: 'ok', host: 'localhost', port: 6380, durationMs: 10 },
    { name: 'pgvector', status: 'skipped', durationMs: 0 },
  ],
};

const RED_REPORT: TierHealthReport = {
  tier: 'standalone',
  configPath: '/some/mosaic.config.json',
  overall: 'red',
  services: [
    {
      name: 'postgres',
      status: 'fail',
      host: 'localhost',
      port: 5432,
      durationMs: 5001,
      error: {
        message: 'connection refused',
        remediation: 'Start Postgres: `docker compose ...`',
      },
    },
    { name: 'valkey', status: 'ok', host: 'localhost', port: 6380, durationMs: 8 },
    { name: 'pgvector', status: 'skipped', durationMs: 0 },
  ],
};

const FEDERATED_GREEN_REPORT: TierHealthReport = {
  tier: 'federated',
  configPath: '/some/mosaic.config.json',
  overall: 'green',
  services: [
    { name: 'postgres', status: 'ok', host: 'localhost', port: 5433, durationMs: 30 },
    { name: 'valkey', status: 'ok', host: 'localhost', port: 6380, durationMs: 5 },
    { name: 'pgvector', status: 'ok', host: 'localhost', port: 5433, durationMs: 25 },
  ],
};

/* ------------------------------------------------------------------ */
/*  Process helpers                                                     */
/* ------------------------------------------------------------------ */

let stdoutCapture = '';
let exitCode: number | undefined;

function captureOutput(): void {
  stdoutCapture = '';
  exitCode = undefined;

  vi.spyOn(process.stdout, 'write').mockImplementation((chunk) => {
    stdoutCapture += typeof chunk === 'string' ? chunk : chunk.toString();
    return true;
  });
  vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
  vi.spyOn(process, 'exit').mockImplementation((code?: string | number | null) => {
    exitCode = typeof code === 'number' ? code : code != null ? Number(code) : undefined;
    throw new Error(`process.exit(${String(code)})`);
  });
  vi.spyOn(console, 'log').mockImplementation((...args: unknown[]) => {
    stdoutCapture += args.join(' ') + '\n';
  });
}

/* ------------------------------------------------------------------ */
/*  Tests                                                               */
/* ------------------------------------------------------------------ */

describe('runGatewayDoctor', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    captureOutput();

    // By default: no config file on disk (env-detection path)
    mocks.mockExistsSync.mockReturnValue(false);
    mocks.mockLoadConfig.mockReturnValue(STANDALONE_CONFIG);
    mocks.mockProbeServiceHealth.mockResolvedValue(GREEN_REPORT);
  });

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

  /* ---------------------------------------------------------------- */
  /*  1. JSON mode: parseable JSON matching the schema                  */
  /* ---------------------------------------------------------------- */

  it('JSON mode emits parseable JSON matching TierHealthReport schema', async () => {
    mocks.mockProbeServiceHealth.mockResolvedValue(GREEN_REPORT);

    await runGatewayDoctor({ json: true });

    const parsed = JSON.parse(stdoutCapture) as TierHealthReport;
    expect(parsed.tier).toBe('standalone');
    expect(parsed.overall).toBe('green');
    expect(Array.isArray(parsed.services)).toBe(true);
    expect(parsed.services).toHaveLength(3);

    // Validate shape of each service check
    for (const svc of parsed.services) {
      expect(['postgres', 'valkey', 'pgvector']).toContain(svc.name);
      expect(['ok', 'fail', 'skipped']).toContain(svc.status);
      expect(typeof svc.durationMs).toBe('number');
    }

    // JSON mode must be silent on console.log — output goes to process.stdout only.
    expect(console.log).not.toHaveBeenCalled();
  });

  it('JSON mode for federated with 3 ok services', async () => {
    mocks.mockProbeServiceHealth.mockResolvedValue(FEDERATED_GREEN_REPORT);

    await runGatewayDoctor({ json: true });

    const parsed = JSON.parse(stdoutCapture) as TierHealthReport;
    expect(parsed.tier).toBe('federated');
    expect(parsed.overall).toBe('green');
    expect(parsed.services.every((s) => s.status === 'ok')).toBe(true);

    // JSON mode must be silent on console.log — output goes to process.stdout only.
    expect(console.log).not.toHaveBeenCalled();
  });

  /* ---------------------------------------------------------------- */
  /*  2. Plain text mode: service lines and overall verdict             */
  /* ---------------------------------------------------------------- */

  it('plain text mode includes service lines for each service', async () => {
    mocks.mockProbeServiceHealth.mockResolvedValue(GREEN_REPORT);

    await runGatewayDoctor({});

    expect(stdoutCapture).toContain('postgres');
    expect(stdoutCapture).toContain('valkey');
    expect(stdoutCapture).toContain('pgvector');
  });

  it('plain text mode includes Overall verdict', async () => {
    mocks.mockProbeServiceHealth.mockResolvedValue(GREEN_REPORT);

    await runGatewayDoctor({});

    expect(stdoutCapture).toContain('Overall: GREEN');
  });

  it('plain text mode shows tier and config path in header', async () => {
    mocks.mockProbeServiceHealth.mockResolvedValue(GREEN_REPORT);

    await runGatewayDoctor({});

    expect(stdoutCapture).toContain('Tier: standalone');
  });

  it('plain text mode shows remediation for failed services', async () => {
    mocks.mockProbeServiceHealth.mockResolvedValue(RED_REPORT);

    try {
      await runGatewayDoctor({});
    } catch {
      // process.exit throws in test
    }

    expect(stdoutCapture).toContain('Remediations:');
    expect(stdoutCapture).toContain('Start Postgres');
  });

  /* ---------------------------------------------------------------- */
  /*  3. Exit codes                                                     */
  /* ---------------------------------------------------------------- */

  it('exits with code 1 when overall is red', async () => {
    mocks.mockProbeServiceHealth.mockResolvedValue(RED_REPORT);

    await expect(runGatewayDoctor({})).rejects.toThrow('process.exit(1)');
    expect(exitCode).toBe(1);
  });

  it('exits with code 0 (no exit call) when overall is green', async () => {
    mocks.mockProbeServiceHealth.mockResolvedValue(GREEN_REPORT);

    await runGatewayDoctor({});

    // process.exit should NOT have been called for green.
    expect(exitCode).toBeUndefined();
  });

  it('JSON mode exits with code 1 when overall is red', async () => {
    mocks.mockProbeServiceHealth.mockResolvedValue(RED_REPORT);

    await expect(runGatewayDoctor({ json: true })).rejects.toThrow('process.exit(1)');
    expect(exitCode).toBe(1);
  });

  /* ---------------------------------------------------------------- */
  /*  4. --config path override is honored                              */
  /* ---------------------------------------------------------------- */

  it('passes --config path to loadConfig when provided', async () => {
    const customPath = '/custom/path/mosaic.config.json';

    await runGatewayDoctor({ config: customPath });

    // loadConfig should have been called with the resolved custom path.
    expect(mocks.mockLoadConfig).toHaveBeenCalledWith(
      expect.stringContaining('mosaic.config.json'),
    );
    // The exact call should include the custom path (resolved).
    const [calledPath] = mocks.mockLoadConfig.mock.calls[0] as [string | undefined];
    expect(calledPath).toContain('custom/path/mosaic.config.json');
  });

  it('calls loadConfig without path when no --config and no file on disk', async () => {
    mocks.mockExistsSync.mockReturnValue(false);

    await runGatewayDoctor({});

    const [calledPath] = mocks.mockLoadConfig.mock.calls[0] as [string | undefined];
    // When no file found, resolveConfigPath returns undefined, so loadConfig is called with undefined
    expect(calledPath).toBeUndefined();
  });

  it('finds config from cwd when mosaic.config.json exists there', async () => {
    // First candidate (cwd/mosaic.config.json) exists.
    mocks.mockExistsSync.mockImplementation((p: unknown) => {
      return typeof p === 'string' && p.endsWith('mosaic.config.json');
    });

    await runGatewayDoctor({});

    const [calledPath] = mocks.mockLoadConfig.mock.calls[0] as [string | undefined];
    expect(calledPath).toBeDefined();
    expect(typeof calledPath).toBe('string');
    expect(calledPath!.endsWith('mosaic.config.json')).toBe(true);
  });
});
