/**
 * Unit tests for the 95% usage-exhaustion helpers (Claude + Codex).
 *
 * These do not exercise the real disk cache or rotation state; they mock
 * just enough to verify the gate's truth-table:
 *
 *   - no accounts            → false (don't block)
 *   - missing data on any    → false (don't block)
 *   - rate-limited account   → counts as exhausted
 *   - any account < 95%      → false (route there instead)
 *   - all accounts ≥ 95%     → true, with earliest expected recovery time
 */
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

vi.mock('./logger.js', () => ({
  logger: {
    info: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
    debug: vi.fn(),
  },
}));

vi.mock('./config.js', () => ({
  DATA_DIR: '/tmp/ejclaw-usage-exhaustion-data',
}));

const { getAllTokensMock } = vi.hoisted(() => ({
  getAllTokensMock: vi.fn(),
}));
vi.mock('./token-rotation.js', () => ({
  getAllTokens: getAllTokensMock,
  getCurrentToken: () => null,
  getConfiguredClaudeTokens: () => [],
}));

const { getAllCodexAccountsMock } = vi.hoisted(() => ({
  getAllCodexAccountsMock: vi.fn(),
}));
vi.mock('./codex-token-rotation.js', () => ({
  getAllCodexAccounts: getAllCodexAccountsMock,
  getCodexAuthPath: () => null,
  updateCodexAccountUsage: vi.fn(),
}));

const { readJsonFileMock } = vi.hoisted(() => ({
  readJsonFileMock: vi.fn(),
}));
vi.mock('./utils.js', async () => {
  const actual =
    await vi.importActual<typeof import('./utils.js')>('./utils.js');
  return {
    ...actual,
    readJsonFile: readJsonFileMock,
    writeJsonFile: vi.fn(),
  };
});

afterEach(() => {
  vi.resetModules();
  vi.clearAllMocks();
});

describe('getClaudeUsageExhaustion', () => {
  beforeEach(() => {
    readJsonFileMock.mockReturnValue({});
  });

  it('returns exhausted=false when no tokens are configured', async () => {
    getAllTokensMock.mockReturnValue([]);
    const { getClaudeUsageExhaustion } = await import('./claude-usage.js');
    expect(getClaudeUsageExhaustion()).toEqual({
      exhausted: false,
      nextResetAt: null,
    });
  });

  it('returns exhausted=false when an account has no cached usage', async () => {
    getAllTokensMock.mockReturnValue([
      {
        token: 'sk-ant-oat01-aaaaaaaa',
        index: 0,
        masked: 'aaa',
        isActive: true,
        isRateLimited: false,
      },
    ]);
    readJsonFileMock.mockReturnValue({}); // empty cache
    const { getClaudeUsageExhaustion } = await import('./claude-usage.js');
    expect(getClaudeUsageExhaustion().exhausted).toBe(false);
  });

  it('returns exhausted=true with the earliest recovery when all accounts are exhausted', async () => {
    const reset0 = new Date('2026-04-28T01:00:00Z').toISOString();
    const reset1 = new Date('2026-04-28T03:00:00Z').toISOString();
    getAllTokensMock.mockReturnValue([
      {
        token: 'sk-ant-oat01-aaaaaaaa',
        index: 0,
        masked: 'aaa',
        isActive: true,
        isRateLimited: false,
      },
      {
        token: 'sk-ant-oat01-bbbbbbbb',
        index: 1,
        masked: 'bbb',
        isActive: false,
        isRateLimited: false,
      },
    ]);
    readJsonFileMock.mockReturnValue({
      'account-0': {
        usage: {
          five_hour: { utilization: 96, resets_at: reset0 },
          seven_day: { utilization: 50, resets_at: '' },
        },
        fetchedAt: Date.now(),
      },
      'account-1': {
        usage: {
          five_hour: { utilization: 30, resets_at: '' },
          seven_day: { utilization: 99, resets_at: reset1 },
        },
        fetchedAt: Date.now(),
      },
    });
    const { getClaudeUsageExhaustion } = await import('./claude-usage.js');
    const info = getClaudeUsageExhaustion();
    expect(info.exhausted).toBe(true);
    // Earliest recovery is account-0 (5h reset at reset0).
    expect(info.nextResetAt).toBe(reset0);
  });

  it('returns exhausted=false when at least one account still has headroom', async () => {
    getAllTokensMock.mockReturnValue([
      {
        token: 'sk-ant-oat01-aaaaaaaa',
        index: 0,
        masked: 'aaa',
        isActive: true,
        isRateLimited: false,
      },
      {
        token: 'sk-ant-oat01-bbbbbbbb',
        index: 1,
        masked: 'bbb',
        isActive: false,
        isRateLimited: false,
      },
    ]);
    readJsonFileMock.mockReturnValue({
      'account-0': {
        usage: {
          five_hour: { utilization: 99 },
          seven_day: { utilization: 99 },
        },
        fetchedAt: Date.now(),
      },
      'account-1': {
        usage: {
          five_hour: { utilization: 80 },
          seven_day: { utilization: 70 },
        },
        fetchedAt: Date.now(),
      },
    });
    const { getClaudeUsageExhaustion } = await import('./claude-usage.js');
    expect(getClaudeUsageExhaustion().exhausted).toBe(false);
  });

  it('treats rate-limited accounts as exhausted', async () => {
    getAllTokensMock.mockReturnValue([
      {
        token: 'sk-ant-oat01-aaaaaaaa',
        index: 0,
        masked: 'aaa',
        isActive: false,
        isRateLimited: true,
      },
    ]);
    readJsonFileMock.mockReturnValue({});
    const { getClaudeUsageExhaustion } = await import('./claude-usage.js');
    expect(getClaudeUsageExhaustion().exhausted).toBe(true);
  });

  it('handles utilization on the 0–1 scale (legacy fractional form)', async () => {
    getAllTokensMock.mockReturnValue([
      {
        token: 'sk-ant-oat01-aaaaaaaa',
        index: 0,
        masked: 'aaa',
        isActive: true,
        isRateLimited: false,
      },
    ]);
    readJsonFileMock.mockReturnValue({
      'account-0': {
        usage: {
          five_hour: { utilization: 0.97 },
          seven_day: { utilization: 0.4 },
        },
        fetchedAt: Date.now(),
      },
    });
    const { getClaudeUsageExhaustion } = await import('./claude-usage.js');
    expect(getClaudeUsageExhaustion().exhausted).toBe(true);
  });

  it('treats percent-scale utilization=1 as 1%, not 100%', async () => {
    getAllTokensMock.mockReturnValue([
      {
        token: 'sk-ant-oat01-aaaaaaaa',
        index: 0,
        masked: 'aaa',
        isActive: true,
        isRateLimited: false,
      },
    ]);
    readJsonFileMock.mockReturnValue({
      'account-0': {
        usage: {
          five_hour: { utilization: 1 },
          seven_day: { utilization: 40 },
        },
        fetchedAt: Date.now(),
      },
    });
    const { getClaudeUsageExhaustion } = await import('./claude-usage.js');
    expect(getClaudeUsageExhaustion().exhausted).toBe(false);
  });
});

describe('getCodexUsageExhaustion', () => {
  it('returns exhausted=false when no Codex accounts are configured', async () => {
    getAllCodexAccountsMock.mockReturnValue([]);
    const { getCodexUsageExhaustion } =
      await import('./codex-usage-collector.js');
    expect(getCodexUsageExhaustion()).toEqual({
      exhausted: false,
      nextResetAt: null,
    });
  });

  it('returns exhausted=false when an account has unknown usage (-1)', async () => {
    getAllCodexAccountsMock.mockReturnValue([
      {
        index: 0,
        accountId: 'a',
        planType: 'pro',
        isActive: true,
        isRateLimited: false,
        cachedUsagePct: -1,
        cachedUsageD7Pct: -1,
      },
    ]);
    const { getCodexUsageExhaustion } =
      await import('./codex-usage-collector.js');
    expect(getCodexUsageExhaustion().exhausted).toBe(false);
  });

  it('returns exhausted=true with earliest recovery when all accounts are exhausted', async () => {
    const reset0 = new Date('2026-04-28T02:00:00Z').toISOString();
    const reset1 = new Date('2026-04-28T05:00:00Z').toISOString();
    getAllCodexAccountsMock.mockReturnValue([
      {
        index: 0,
        accountId: 'a',
        planType: 'pro',
        isActive: true,
        isRateLimited: false,
        cachedUsagePct: 96,
        cachedUsageD7Pct: 40,
        resetAt: reset0,
      },
      {
        index: 1,
        accountId: 'b',
        planType: 'pro',
        isActive: false,
        isRateLimited: false,
        cachedUsagePct: 30,
        cachedUsageD7Pct: 99,
        resetD7At: reset1,
      },
    ]);
    const { getCodexUsageExhaustion } =
      await import('./codex-usage-collector.js');
    const info = getCodexUsageExhaustion();
    expect(info.exhausted).toBe(true);
    expect(info.nextResetAt).toBe(reset0);
  });

  it('returns exhausted=false when at least one Codex account has headroom', async () => {
    getAllCodexAccountsMock.mockReturnValue([
      {
        index: 0,
        accountId: 'a',
        planType: 'pro',
        isActive: true,
        isRateLimited: false,
        cachedUsagePct: 99,
        cachedUsageD7Pct: 99,
      },
      {
        index: 1,
        accountId: 'b',
        planType: 'pro',
        isActive: false,
        isRateLimited: false,
        cachedUsagePct: 50,
        cachedUsageD7Pct: 80,
      },
    ]);
    const { getCodexUsageExhaustion } =
      await import('./codex-usage-collector.js');
    expect(getCodexUsageExhaustion().exhausted).toBe(false);
  });

  it('treats rate-limited Codex accounts as exhausted', async () => {
    getAllCodexAccountsMock.mockReturnValue([
      {
        index: 0,
        accountId: 'a',
        planType: 'pro',
        isActive: false,
        isRateLimited: true,
        cachedUsagePct: 10,
        cachedUsageD7Pct: 10,
      },
    ]);
    const { getCodexUsageExhaustion } =
      await import('./codex-usage-collector.js');
    expect(getCodexUsageExhaustion().exhausted).toBe(true);
  });
});
