import { describe, it, expect, vi, beforeEach } from 'vitest';

// ─── Mocks ──────────────────────────────────────────────────────────────────

vi.mock('../../auth.js', () => ({
  loadSession: vi.fn(),
  validateSession: vi.fn(),
  signIn: vi.fn(),
  saveSession: vi.fn(),
}));

vi.mock('./daemon.js', () => ({
  readMeta: vi.fn(),
  writeMeta: vi.fn(),
}));

vi.mock('./login.js', () => ({
  getGatewayUrl: vi.fn().mockReturnValue('http://localhost:14242'),
  // promptLine/promptSecret are used by ensureSession; return fixed values so tests don't block on stdin
  promptLine: vi.fn().mockResolvedValue('test@example.com'),
  promptSecret: vi.fn().mockResolvedValue('test-password'),
}));

const mockFetch = vi.fn();
vi.stubGlobal('fetch', mockFetch);

import { runRecoverToken, ensureSession } from './token-ops.js';
import { loadSession, validateSession, signIn, saveSession } from '../../auth.js';
import { readMeta, writeMeta } from './daemon.js';

const mockLoadSession = vi.mocked(loadSession);
const mockValidateSession = vi.mocked(validateSession);
const mockSignIn = vi.mocked(signIn);
const mockSaveSession = vi.mocked(saveSession);
const mockReadMeta = vi.mocked(readMeta);
const mockWriteMeta = vi.mocked(writeMeta);

const baseUrl = 'http://localhost:14242';
const fakeCookie = 'better-auth.session_token=sess123';
const fakeToken = {
  id: 'tok-1',
  label: 'CLI recovery token (2026-04-04 12:00)',
  plaintext: 'abcdef1234567890',
};
const fakeMeta = {
  version: '1.0.0',
  installedAt: '',
  entryPoint: '',
  host: 'localhost',
  port: 14242,
};

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

  it('returns cookie from stored session when valid', async () => {
    mockLoadSession.mockReturnValueOnce({ cookie: fakeCookie, userId: 'u1', email: 'a@b.com' });
    mockValidateSession.mockResolvedValueOnce(true);

    const cookie = await ensureSession(baseUrl);
    expect(cookie).toBe(fakeCookie);
    expect(mockSignIn).not.toHaveBeenCalled();
  });

  it('prompts for credentials and signs in when stored session is invalid', async () => {
    mockLoadSession.mockReturnValueOnce({ cookie: 'old-cookie', userId: 'u1', email: 'a@b.com' });
    mockValidateSession.mockResolvedValueOnce(false);
    const newAuth = { cookie: fakeCookie, userId: 'u2', email: 'a@b.com' };
    mockSignIn.mockResolvedValueOnce(newAuth);

    const cookie = await ensureSession(baseUrl);
    expect(cookie).toBe(fakeCookie);
    expect(mockSaveSession).toHaveBeenCalledWith(baseUrl, newAuth);
  });

  it('prompts for credentials when no session exists', async () => {
    mockLoadSession.mockReturnValueOnce(null);
    const newAuth = { cookie: fakeCookie, userId: 'u2', email: 'a@b.com' };
    mockSignIn.mockResolvedValueOnce(newAuth);

    const cookie = await ensureSession(baseUrl);
    expect(cookie).toBe(fakeCookie);
    expect(mockSignIn).toHaveBeenCalled();
  });

  it('exits non-zero when signIn fails', async () => {
    mockLoadSession.mockReturnValueOnce(null);
    mockSignIn.mockRejectedValueOnce(new Error('Sign-in failed (401): bad creds'));
    const processExitSpy = vi
      .spyOn(process, 'exit')
      .mockImplementation((_code?: number | string | null | undefined) => {
        throw new Error(`process.exit(${String(_code)})`);
      });
    const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});

    await expect(ensureSession(baseUrl)).rejects.toThrow('process.exit(2)');
    expect(processExitSpy).toHaveBeenCalledWith(2);

    processExitSpy.mockRestore();
    consoleErrorSpy.mockRestore();
  });
});

describe('runRecoverToken', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    vi.spyOn(console, 'log').mockImplementation(() => {});
    vi.spyOn(console, 'error').mockImplementation(() => {});
  });

  it('prompts for login, mints a token, and persists it when no session exists', async () => {
    mockLoadSession.mockReturnValueOnce(null);
    const newAuth = { cookie: fakeCookie, userId: 'u2', email: 'admin@test.com' };
    mockSignIn.mockResolvedValueOnce(newAuth);
    mockReadMeta.mockReturnValue(fakeMeta);
    mockFetch.mockResolvedValueOnce({
      ok: true,
      status: 200,
      json: async () => fakeToken,
    });

    await runRecoverToken();

    expect(mockSignIn).toHaveBeenCalled();
    expect(mockFetch).toHaveBeenCalledWith(
      `${baseUrl}/api/admin/tokens`,
      expect.objectContaining({ method: 'POST' }),
    );
    expect(mockWriteMeta).toHaveBeenCalledWith(
      expect.objectContaining({ adminToken: fakeToken.plaintext }),
    );
  });

  it('skips login when a valid session exists and mints a recovery token', async () => {
    mockLoadSession.mockReturnValueOnce({ cookie: fakeCookie, userId: 'u1', email: 'a@b.com' });
    mockValidateSession.mockResolvedValueOnce(true);
    mockReadMeta.mockReturnValue(fakeMeta);
    mockFetch.mockResolvedValueOnce({
      ok: true,
      status: 200,
      json: async () => fakeToken,
    });

    await runRecoverToken();

    expect(mockSignIn).not.toHaveBeenCalled();
    expect(mockWriteMeta).toHaveBeenCalledWith(
      expect.objectContaining({ adminToken: fakeToken.plaintext }),
    );
  });

  it('uses label containing "recovery token"', async () => {
    mockLoadSession.mockReturnValueOnce({ cookie: fakeCookie, userId: 'u1', email: 'a@b.com' });
    mockValidateSession.mockResolvedValueOnce(true);
    mockReadMeta.mockReturnValue(fakeMeta);
    mockFetch.mockResolvedValueOnce({
      ok: true,
      status: 200,
      json: async () => fakeToken,
    });

    await runRecoverToken();

    const call = mockFetch.mock.calls[0] as [string, RequestInit];
    const body = JSON.parse(call[1].body as string) as { label: string };
    expect(body.label).toMatch(/CLI recovery token/);
  });
});
