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'),
}));

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

import { runRotateToken, mintAdminToken, persistToken } from './token-ops.js';
import { loadSession, validateSession } from '../../auth.js';
import { readMeta, writeMeta } from './daemon.js';

const mockLoadSession = vi.mocked(loadSession);
const mockValidateSession = vi.mocked(validateSession);
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 rotated token (2026-04-04)',
  plaintext: 'abcdef1234567890',
};
const fakeMeta = {
  version: '1.0.0',
  installedAt: '',
  entryPoint: '',
  host: 'localhost',
  port: 14242,
};

describe('mintAdminToken', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('calls the admin tokens endpoint with the session cookie and returns the token', async () => {
    mockFetch.mockResolvedValueOnce({
      ok: true,
      status: 200,
      json: async () => fakeToken,
    });

    const result = await mintAdminToken(baseUrl, fakeCookie, fakeToken.label);

    expect(mockFetch).toHaveBeenCalledWith(
      `${baseUrl}/api/admin/tokens`,
      expect.objectContaining({
        method: 'POST',
        headers: expect.objectContaining({ Cookie: fakeCookie }),
      }),
    );
    expect(result).toEqual(fakeToken);
  });

  it('exits 2 on 401 from the server', async () => {
    mockFetch.mockResolvedValueOnce({ ok: false, status: 401, text: async () => 'Unauthorized' });
    const processExitSpy = vi
      .spyOn(process, 'exit')
      .mockImplementation((_code?: number | string | null | undefined) => {
        throw new Error(`process.exit(${String(_code)})`);
      });

    await expect(mintAdminToken(baseUrl, fakeCookie, 'label')).rejects.toThrow('process.exit(2)');
    expect(processExitSpy).toHaveBeenCalledWith(2);
    processExitSpy.mockRestore();
  });

  it('exits 2 on 403 from the server', async () => {
    mockFetch.mockResolvedValueOnce({ ok: false, status: 403, text: async () => 'Forbidden' });
    const processExitSpy = vi
      .spyOn(process, 'exit')
      .mockImplementation((_code?: number | string | null | undefined) => {
        throw new Error(`process.exit(${String(_code)})`);
      });

    await expect(mintAdminToken(baseUrl, fakeCookie, 'label')).rejects.toThrow('process.exit(2)');
    expect(processExitSpy).toHaveBeenCalledWith(2);
    processExitSpy.mockRestore();
  });

  it('exits 3 on other non-ok status', async () => {
    mockFetch.mockResolvedValueOnce({ ok: false, status: 500, text: async () => 'Internal Error' });
    const processExitSpy = vi
      .spyOn(process, 'exit')
      .mockImplementation((_code?: number | string | null | undefined) => {
        throw new Error(`process.exit(${String(_code)})`);
      });

    await expect(mintAdminToken(baseUrl, fakeCookie, 'label')).rejects.toThrow('process.exit(3)');
    expect(processExitSpy).toHaveBeenCalledWith(3);
    processExitSpy.mockRestore();
  });

  it('exits 1 on network error', async () => {
    mockFetch.mockRejectedValueOnce(new Error('connection refused'));
    const processExitSpy = vi
      .spyOn(process, 'exit')
      .mockImplementation((_code?: number | string | null | undefined) => {
        throw new Error(`process.exit(${String(_code)})`);
      });

    await expect(mintAdminToken(baseUrl, fakeCookie, 'label')).rejects.toThrow('process.exit(1)');
    expect(processExitSpy).toHaveBeenCalledWith(1);
    processExitSpy.mockRestore();
  });
});

describe('persistToken', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('writes the new token to meta.json', () => {
    mockReadMeta.mockReturnValueOnce(fakeMeta);
    const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});

    persistToken(baseUrl, fakeToken);

    expect(mockWriteMeta).toHaveBeenCalledWith(
      expect.objectContaining({ adminToken: fakeToken.plaintext }),
    );
    consoleSpy.mockRestore();
  });

  it('prints a masked preview of the token', () => {
    mockReadMeta.mockReturnValueOnce(fakeMeta);
    const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});

    persistToken(baseUrl, fakeToken);

    const allOutput = consoleSpy.mock.calls.map((c) => c.join(' ')).join('\n');
    expect(allOutput).toContain('abcdef12...');
    consoleSpy.mockRestore();
  });
});

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

  it('exits 2 when there is no stored session', async () => {
    mockLoadSession.mockReturnValueOnce(null);
    const processExitSpy = vi
      .spyOn(process, 'exit')
      .mockImplementation((_code?: number | string | null | undefined) => {
        throw new Error(`process.exit(${String(_code)})`);
      });

    await expect(runRotateToken()).rejects.toThrow('process.exit(2)');
    expect(processExitSpy).toHaveBeenCalledWith(2);
    processExitSpy.mockRestore();
  });

  it('exits 2 when session is invalid', async () => {
    mockLoadSession.mockReturnValueOnce({ cookie: fakeCookie, userId: 'u1', email: 'a@b.com' });
    mockValidateSession.mockResolvedValueOnce(false);
    const processExitSpy = vi
      .spyOn(process, 'exit')
      .mockImplementation((_code?: number | string | null | undefined) => {
        throw new Error(`process.exit(${String(_code)})`);
      });

    await expect(runRotateToken()).rejects.toThrow('process.exit(2)');
    expect(processExitSpy).toHaveBeenCalledWith(2);
    processExitSpy.mockRestore();
  });

  it('mints and persists a new token when session is valid', 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 runRotateToken();

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