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

// ── Mock daemon module ────────────────────────────────────────────────────

const daemonState = {
  meta: null as null | {
    version: string;
    installedAt: string;
    entryPoint: string;
    host: string;
    port: number;
    adminToken?: string;
  },
  writeMetaCalls: [] as unknown[],
};

vi.mock('../commands/gateway/daemon.js', () => ({
  GATEWAY_HOME: '/tmp/fake-gw',
  readMeta: () => daemonState.meta,
  writeMeta: (m: unknown) => {
    daemonState.writeMetaCalls.push(m);
  },
}));

// ── Mock masked-prompt so we never touch real stdin raw mode ──────────────

vi.mock('../prompter/masked-prompt.js', () => ({
  promptMaskedConfirmed: vi.fn().mockResolvedValue('supersecret'),
}));

import { gatewayBootstrapStage } from './gateway-bootstrap.js';

// ── Helpers ───────────────────────────────────────────────────────────────

function buildPrompter(overrides: Partial<Record<string, unknown>> = {}) {
  return {
    intro: vi.fn(),
    outro: vi.fn(),
    note: vi.fn(),
    log: vi.fn(),
    warn: vi.fn(),
    text: vi.fn().mockImplementation(async (opts: { message: string }) => {
      if (/name/i.test(opts.message)) return 'Tester';
      if (/email/i.test(opts.message)) return 'test@example.com';
      return '';
    }),
    confirm: vi.fn().mockResolvedValue(true),
    select: vi.fn(),
    multiselect: vi.fn(),
    groupMultiselect: vi.fn(),
    spinner: vi.fn().mockReturnValue({ update: vi.fn(), stop: vi.fn() }),
    separator: vi.fn(),
    ...overrides,
  };
}

function makeState(): WizardState {
  return {
    mosaicHome: '/tmp/fake-mosaic',
    sourceDir: '/tmp/fake-mosaic',
    mode: 'quick',
    installAction: 'fresh',
    soul: {},
    user: {},
    tools: {},
    runtimes: { detected: [], mcpConfigured: false },
    selectedSkills: [],
  };
}

// ── Tests ─────────────────────────────────────────────────────────────────

describe('gatewayBootstrapStage', () => {
  const originalEnv = { ...process.env };
  const originalFetch = globalThis.fetch;

  beforeEach(() => {
    daemonState.meta = {
      version: '0.0.99',
      installedAt: new Date().toISOString(),
      entryPoint: '/fake/entry.js',
      host: 'localhost',
      port: 14242,
    };
    daemonState.writeMetaCalls = [];
    // Keep headless so we exercise the env-var path
    process.env['MOSAIC_ASSUME_YES'] = '1';
    process.env['MOSAIC_ADMIN_NAME'] = 'Tester';
    process.env['MOSAIC_ADMIN_EMAIL'] = 'test@example.com';
    process.env['MOSAIC_ADMIN_PASSWORD'] = 'supersecret';
  });

  afterEach(() => {
    process.env = { ...originalEnv };
    globalThis.fetch = originalFetch;
    vi.restoreAllMocks();
  });

  it('creates the first admin user and persists the token', async () => {
    const fetchMock = vi
      .fn()
      .mockImplementationOnce(async () => ({
        ok: true,
        json: async () => ({ needsSetup: true }),
      }))
      .mockImplementationOnce(async () => ({
        ok: true,
        json: async () => ({
          user: { id: 'u1', email: 'test@example.com' },
          token: { plaintext: 'plain-token-xyz' },
        }),
      }));
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    globalThis.fetch = fetchMock as any;

    const p = buildPrompter();
    const state = makeState();

    const result = await gatewayBootstrapStage(p, state, { host: 'localhost', port: 14242 });

    expect(result.completed).toBe(true);
    expect(fetchMock).toHaveBeenCalledTimes(2);
    expect(daemonState.writeMetaCalls).toHaveLength(1);
    const persistedMeta = daemonState.writeMetaCalls[0] as { adminToken?: string };
    expect(persistedMeta.adminToken).toBe('plain-token-xyz');
    expect(state.gateway?.adminTokenIssued).toBe(true);
  });

  it('short-circuits when admin already exists and token is on file', async () => {
    daemonState.meta!.adminToken = 'already-have-token';
    const fetchMock = vi.fn().mockResolvedValueOnce({
      ok: true,
      json: async () => ({ needsSetup: false }),
    });
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    globalThis.fetch = fetchMock as any;

    const p = buildPrompter();
    const state = makeState();

    const result = await gatewayBootstrapStage(p, state, { host: 'localhost', port: 14242 });

    expect(result.completed).toBe(true);
    expect(fetchMock).toHaveBeenCalledTimes(1);
    expect(daemonState.writeMetaCalls).toHaveLength(0);
  });

  it('treats headless rerun of already-bootstrapped gateway as a successful no-op', async () => {
    // Admin already exists server-side, but local meta has no token cache.
    // Headless mode should NOT fail the install — leave admin in place.
    daemonState.meta!.adminToken = undefined;
    const fetchMock = vi.fn().mockResolvedValueOnce({
      ok: true,
      json: async () => ({ needsSetup: false }),
    });
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    globalThis.fetch = fetchMock as any;

    const p = buildPrompter();
    const state = makeState();

    const result = await gatewayBootstrapStage(p, state, { host: 'localhost', port: 14242 });

    expect(result.completed).toBe(true);
    expect(fetchMock).toHaveBeenCalledTimes(1);
    expect(daemonState.writeMetaCalls).toHaveLength(0);
  });

  it('returns non-completed in headless mode when required env vars are missing', async () => {
    delete process.env['MOSAIC_ADMIN_NAME'];
    const fetchMock = vi.fn().mockResolvedValueOnce({
      ok: true,
      json: async () => ({ needsSetup: true }),
    });
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    globalThis.fetch = fetchMock as any;

    const p = buildPrompter();
    const state = makeState();

    const result = await gatewayBootstrapStage(p, state, { host: 'localhost', port: 14242 });

    expect(result.completed).toBe(false);
    expect(p.warn).toHaveBeenCalledWith(expect.stringContaining('MOSAIC_ADMIN_NAME'));
  });

  it('returns non-completed when bootstrap status call fails', async () => {
    const fetchMock = vi.fn().mockRejectedValueOnce(new Error('network down'));
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    globalThis.fetch = fetchMock as any;

    const p = buildPrompter();
    const state = makeState();

    const result = await gatewayBootstrapStage(p, state, { host: 'localhost', port: 14242 });

    expect(result.completed).toBe(false);
    expect(p.warn).toHaveBeenCalled();
  });

  it('returns non-completed when bootstrap/setup responds with error', async () => {
    const fetchMock = vi
      .fn()
      .mockResolvedValueOnce({
        ok: true,
        json: async () => ({ needsSetup: true }),
      })
      .mockResolvedValueOnce({
        ok: false,
        status: 400,
        text: async () => 'bad password',
      });
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    globalThis.fetch = fetchMock as any;

    const p = buildPrompter();
    const state = makeState();

    const result = await gatewayBootstrapStage(p, state, { host: 'localhost', port: 14242 });

    expect(result.completed).toBe(false);
    expect(daemonState.writeMetaCalls).toHaveLength(0);
  });
});
