/**
 * CU-06-05 — Vitest tests for mosaic telemetry command
 */

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { Command } from 'commander';
import { registerTelemetryCommand } from './telemetry.js';
import type { TelemetryConsent } from '../telemetry/consent-store.js';

// ─── module mocks ─────────────────────────────────────────────────────────────

// Mock consent-store so tests don't touch the filesystem.
const mockConsent: TelemetryConsent = {
  remoteEnabled: false,
  optedInAt: null,
  optedOutAt: null,
  lastUploadAt: null,
};

vi.mock('../telemetry/consent-store.js', () => ({
  readConsent: vi.fn(() => ({ ...mockConsent })),
  writeConsent: vi.fn(),
  optIn: vi.fn(() => ({
    ...mockConsent,
    remoteEnabled: true,
    optedInAt: '2026-01-01T00:00:00.000Z',
  })),
  optOut: vi.fn(() => ({
    ...mockConsent,
    remoteEnabled: false,
    optedOutAt: '2026-01-01T00:00:00.000Z',
  })),
  recordUpload: vi.fn(),
}));

// Mock the telemetry client shim.
const mockClientInstance = {
  init: vi.fn(),
  captureEvent: vi.fn(),
  upload: vi.fn().mockResolvedValue(undefined),
  shutdown: vi.fn().mockResolvedValue(undefined),
};

vi.mock('../telemetry/client-shim.js', () => ({
  getTelemetryClient: vi.fn(() => mockClientInstance),
  setTelemetryClient: vi.fn(),
  resetTelemetryClient: vi.fn(),
}));

// Mock @clack/prompts so tests don't require stdin.
vi.mock('@clack/prompts', () => ({
  confirm: vi.fn().mockResolvedValue(true),
  intro: vi.fn(),
  outro: vi.fn(),
  isCancel: vi.fn().mockReturnValue(false),
  cancel: vi.fn(),
}));

// ─── helpers ──────────────────────────────────────────────────────────────────

function buildProgram(): Command {
  const program = new Command();
  program.exitOverride();
  registerTelemetryCommand(program);
  return program;
}

function getTelemetryCmd(program: Command): Command {
  const found = program.commands.find((c) => c.name() === 'telemetry');
  if (!found) throw new Error('telemetry command not found');
  return found;
}

function getLocalCmd(telemetryCmd: Command): Command {
  const found = telemetryCmd.commands.find((c) => c.name() === 'local');
  if (!found) throw new Error('local subcommand not found');
  return found;
}

// ─── CU-06-05 a: command structure smoke test ─────────────────────────────────

describe('registerTelemetryCommand — structure', () => {
  it('registers a "telemetry" command on the program', () => {
    const program = buildProgram();
    const names = program.commands.map((c) => c.name());
    expect(names).toContain('telemetry');
  });

  it('registers the expected top-level subcommands', () => {
    const program = buildProgram();
    const tel = getTelemetryCmd(program);
    const subs = tel.commands.map((c) => c.name()).sort();
    expect(subs).toEqual(['local', 'opt-in', 'opt-out', 'status', 'test', 'upload']);
  });

  it('registers all three local subcommands', () => {
    const program = buildProgram();
    const local = getLocalCmd(getTelemetryCmd(program));
    const subs = local.commands.map((c) => c.name()).sort();
    expect(subs).toEqual(['jaeger', 'status', 'tail']);
  });
});

// ─── CU-06-05 b: opt-in / opt-out ────────────────────────────────────────────

describe('telemetry opt-in', () => {
  let consoleSpy: ReturnType<typeof vi.spyOn>;

  beforeEach(async () => {
    vi.clearAllMocks();
    consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);

    // Provide disabled consent so opt-in path is taken.
    const store = await import('../telemetry/consent-store.js');
    vi.mocked(store.readConsent).mockReturnValue({
      remoteEnabled: false,
      optedInAt: null,
      optedOutAt: null,
      lastUploadAt: null,
    });
    vi.mocked(store.optIn).mockReturnValue({
      remoteEnabled: true,
      optedInAt: '2026-01-01T00:00:00.000Z',
      optedOutAt: null,
      lastUploadAt: null,
    });

    const clack = await import('@clack/prompts');
    vi.mocked(clack.confirm).mockResolvedValue(true);
    vi.mocked(clack.isCancel).mockReturnValue(false);
  });

  afterEach(() => {
    consoleSpy.mockRestore();
  });

  it('calls optIn() when user confirms', async () => {
    const program = buildProgram();
    await program.parseAsync(['node', 'mosaic', 'telemetry', 'opt-in']);

    const store = await import('../telemetry/consent-store.js');
    expect(vi.mocked(store.optIn)).toHaveBeenCalled();
  });

  it('does not call optIn() when user cancels', async () => {
    const clack = await import('@clack/prompts');
    vi.mocked(clack.confirm).mockResolvedValue(false);

    const program = buildProgram();
    await program.parseAsync(['node', 'mosaic', 'telemetry', 'opt-in']);

    const store = await import('../telemetry/consent-store.js');
    expect(vi.mocked(store.optIn)).not.toHaveBeenCalled();
  });
});

describe('telemetry opt-out', () => {
  let consoleSpy: ReturnType<typeof vi.spyOn>;

  beforeEach(async () => {
    vi.clearAllMocks();
    consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);

    const store = await import('../telemetry/consent-store.js');
    vi.mocked(store.readConsent).mockReturnValue({
      remoteEnabled: true,
      optedInAt: '2026-01-01T00:00:00.000Z',
      optedOutAt: null,
      lastUploadAt: null,
    });
    vi.mocked(store.optOut).mockReturnValue({
      remoteEnabled: false,
      optedInAt: '2026-01-01T00:00:00.000Z',
      optedOutAt: '2026-02-01T00:00:00.000Z',
      lastUploadAt: null,
    });

    const clack = await import('@clack/prompts');
    vi.mocked(clack.confirm).mockResolvedValue(true);
    vi.mocked(clack.isCancel).mockReturnValue(false);
  });

  afterEach(() => {
    consoleSpy.mockRestore();
  });

  it('calls optOut() when user confirms', async () => {
    const program = buildProgram();
    await program.parseAsync(['node', 'mosaic', 'telemetry', 'opt-out']);

    const store = await import('../telemetry/consent-store.js');
    expect(vi.mocked(store.optOut)).toHaveBeenCalled();
  });

  it('does not call optOut() when already disabled', async () => {
    const store = await import('../telemetry/consent-store.js');
    vi.mocked(store.readConsent).mockReturnValue({
      remoteEnabled: false,
      optedInAt: null,
      optedOutAt: null,
      lastUploadAt: null,
    });

    const program = buildProgram();
    await program.parseAsync(['node', 'mosaic', 'telemetry', 'opt-out']);
    expect(vi.mocked(store.optOut)).not.toHaveBeenCalled();
  });
});

// ─── CU-06-05 c: status ──────────────────────────────────────────────────────

describe('telemetry status', () => {
  let consoleSpy: ReturnType<typeof vi.spyOn>;

  beforeEach(() => {
    vi.clearAllMocks();
    consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
  });

  afterEach(() => {
    consoleSpy.mockRestore();
  });

  it('shows disabled state when remote upload is off', async () => {
    const store = await import('../telemetry/consent-store.js');
    vi.mocked(store.readConsent).mockReturnValue({
      remoteEnabled: false,
      optedInAt: null,
      optedOutAt: null,
      lastUploadAt: null,
    });

    const program = buildProgram();
    await program.parseAsync(['node', 'mosaic', 'telemetry', 'status']);

    const output = consoleSpy.mock.calls.map((c) => c[0] as string).join('\n');
    expect(output).toContain('false');
    expect(output).toContain('(never)');
  });

  it('shows enabled state and timestamps when opted in', async () => {
    const store = await import('../telemetry/consent-store.js');
    vi.mocked(store.readConsent).mockReturnValue({
      remoteEnabled: true,
      optedInAt: '2026-01-01T00:00:00.000Z',
      optedOutAt: null,
      lastUploadAt: '2026-03-01T00:00:00.000Z',
    });

    const program = buildProgram();
    await program.parseAsync(['node', 'mosaic', 'telemetry', 'status']);

    const output = consoleSpy.mock.calls.map((c) => c[0] as string).join('\n');
    expect(output).toContain('true');
    expect(output).toContain('2026-01-01');
    expect(output).toContain('2026-03-01');
  });

  it('shows dry-run banner when MOSAIC_TELEMETRY_DRY_RUN=1', async () => {
    process.env['MOSAIC_TELEMETRY_DRY_RUN'] = '1';

    const store = await import('../telemetry/consent-store.js');
    vi.mocked(store.readConsent).mockReturnValue({
      remoteEnabled: false,
      optedInAt: null,
      optedOutAt: null,
      lastUploadAt: null,
    });

    const program = buildProgram();
    await program.parseAsync(['node', 'mosaic', 'telemetry', 'status']);

    const output = consoleSpy.mock.calls.map((c) => c[0] as string).join('\n');
    expect(output).toContain('[dry-run]');

    delete process.env['MOSAIC_TELEMETRY_DRY_RUN'];
  });
});

// ─── CU-06-05 d: test / upload — dry-run assertions ──────────────────────────

describe('telemetry test (dry-run)', () => {
  let consoleSpy: ReturnType<typeof vi.spyOn>;

  beforeEach(() => {
    vi.clearAllMocks();
    consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
  });

  afterEach(() => {
    consoleSpy.mockRestore();
  });

  it('prints dry-run banner and does not call upload()', async () => {
    const program = buildProgram();
    await program.parseAsync(['node', 'mosaic', 'telemetry', 'test']);

    const output = consoleSpy.mock.calls.map((c) => c[0] as string).join('\n');
    expect(output).toContain('[dry-run]');
    expect(mockClientInstance.upload).not.toHaveBeenCalled();
  });

  it('calls captureEvent() with a mosaic.cli.test event', async () => {
    const program = buildProgram();
    await program.parseAsync(['node', 'mosaic', 'telemetry', 'test']);

    expect(mockClientInstance.captureEvent).toHaveBeenCalledWith(
      expect.objectContaining({ name: 'mosaic.cli.test' }),
    );
  });

  it('does not make network calls in dry-run mode', async () => {
    const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response());

    const program = buildProgram();
    await program.parseAsync(['node', 'mosaic', 'telemetry', 'test']);

    expect(fetchSpy).not.toHaveBeenCalled();
    fetchSpy.mockRestore();
  });
});

describe('telemetry upload (dry-run default)', () => {
  let consoleSpy: ReturnType<typeof vi.spyOn>;

  beforeEach(async () => {
    vi.clearAllMocks();
    consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);

    // Remote disabled by default.
    const store = await import('../telemetry/consent-store.js');
    vi.mocked(store.readConsent).mockReturnValue({
      remoteEnabled: false,
      optedInAt: null,
      optedOutAt: null,
      lastUploadAt: null,
    });
  });

  afterEach(() => {
    consoleSpy.mockRestore();
    delete process.env['MOSAIC_TELEMETRY_DRY_RUN'];
    delete process.env['MOSAIC_TELEMETRY_ENDPOINT'];
  });

  it('prints dry-run banner when remote upload is disabled', async () => {
    const program = buildProgram();
    await program.parseAsync(['node', 'mosaic', 'telemetry', 'upload']);

    const output = consoleSpy.mock.calls.map((c) => c[0] as string).join('\n');
    expect(output).toContain('[dry-run]');
    expect(mockClientInstance.upload).not.toHaveBeenCalled();
  });

  it('prints dry-run banner when MOSAIC_TELEMETRY_DRY_RUN=1 even if opted in', async () => {
    process.env['MOSAIC_TELEMETRY_DRY_RUN'] = '1';
    process.env['MOSAIC_TELEMETRY_ENDPOINT'] = 'http://example.com/telemetry';

    const store = await import('../telemetry/consent-store.js');
    vi.mocked(store.readConsent).mockReturnValue({
      remoteEnabled: true,
      optedInAt: '2026-01-01T00:00:00.000Z',
      optedOutAt: null,
      lastUploadAt: null,
    });

    const program = buildProgram();
    await program.parseAsync(['node', 'mosaic', 'telemetry', 'upload']);

    const output = consoleSpy.mock.calls.map((c) => c[0] as string).join('\n');
    expect(output).toContain('[dry-run]');
    expect(mockClientInstance.upload).not.toHaveBeenCalled();
  });
});

// ─── local subcommand smoke tests ─────────────────────────────────────────────

describe('telemetry local tail', () => {
  let consoleSpy: ReturnType<typeof vi.spyOn>;

  beforeEach(() => {
    vi.clearAllMocks();
    consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
  });

  afterEach(() => {
    consoleSpy.mockRestore();
  });

  it('prints Jaeger UI URL and docker compose hint', async () => {
    const program = buildProgram();
    await program.parseAsync(['node', 'mosaic', 'telemetry', 'local', 'tail']);

    const output = consoleSpy.mock.calls.map((c) => c[0] as string).join('\n');
    expect(output).toContain('Jaeger');
    expect(output).toContain('docker compose');
  });
});

describe('telemetry local jaeger', () => {
  let consoleSpy: ReturnType<typeof vi.spyOn>;

  beforeEach(() => {
    vi.clearAllMocks();
    consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
    delete process.env['JAEGER_UI_URL'];
  });

  afterEach(() => {
    consoleSpy.mockRestore();
  });

  it('prints the default Jaeger URL', async () => {
    const program = buildProgram();
    await program.parseAsync(['node', 'mosaic', 'telemetry', 'local', 'jaeger']);
    expect(consoleSpy).toHaveBeenCalledWith('http://localhost:16686');
  });

  it('respects JAEGER_UI_URL env var', async () => {
    process.env['JAEGER_UI_URL'] = 'http://jaeger.example.com:16686';
    const program = buildProgram();
    await program.parseAsync(['node', 'mosaic', 'telemetry', 'local', 'jaeger']);
    expect(consoleSpy).toHaveBeenCalledWith('http://jaeger.example.com:16686');
    delete process.env['JAEGER_UI_URL'];
  });
});
