import { EventEmitter } from 'events';
import fs from 'fs';
import path from 'path';

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

vi.mock('child_process', () => ({
  spawn: vi.fn(),
}));

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

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

vi.mock('os', async () => {
  const actual = await vi.importActual<typeof import('os')>('os');
  return {
    ...actual,
    default: {
      ...actual,
      homedir: () => process.env.CODEX_USAGE_TEST_HOME || '/tmp',
    },
    homedir: () => process.env.CODEX_USAGE_TEST_HOME || '/tmp',
  };
});

function createDefaultCodexAuth(homeDir: string): string {
  const authPath = path.join(homeDir, '.codex', 'auth.json');
  fs.mkdirSync(path.dirname(authPath), { recursive: true });
  fs.writeFileSync(
    authPath,
    JSON.stringify({
      auth_mode: 'chatgpt',
      tokens: { account_id: 'default-acct', access_token: 'default-token' },
    }),
  );
  return authPath;
}

function createFakeChildProcess(rateLimitsByLimitId: Record<string, unknown>) {
  const proc = new EventEmitter() as EventEmitter & {
    stdout: EventEmitter;
    stdin: { write: ReturnType<typeof vi.fn> };
    kill: ReturnType<typeof vi.fn>;
  };
  const stdout = new EventEmitter();
  proc.stdout = stdout;
  proc.stdin = {
    write: vi.fn((payload: string) => {
      const message = JSON.parse(payload.trim()) as {
        id: number;
        method?: string;
      };
      if (message.id === 1) {
        setImmediate(() => {
          stdout.emit(
            'data',
            Buffer.from(`${JSON.stringify({ id: 1, result: {} })}\n`),
          );
        });
      }
      if (message.id === 2 && message.method === 'account/rateLimits/read') {
        setImmediate(() => {
          stdout.emit(
            'data',
            Buffer.from(
              `${JSON.stringify({
                id: 2,
                result: { rateLimitsByLimitId },
              })}\n`,
            ),
          );
        });
      }
      return true;
    }),
  };
  proc.kill = vi.fn();
  return proc;
}

describe('codex-usage-collector fallback account usage', () => {
  let tempHome: string;

  beforeEach(() => {
    vi.resetModules();
    vi.clearAllMocks();
    tempHome = fs.mkdtempSync(path.join('/tmp', 'ejclaw-codex-usage-'));
    process.env.CODEX_USAGE_TEST_HOME = tempHome;
  });

  afterEach(() => {
    delete process.env.CODEX_USAGE_TEST_HOME;
    fs.rmSync(tempHome, { recursive: true, force: true });
  });

  it('refreshes usage via ~/.codex/auth.json when ~/.codex-accounts is missing', async () => {
    createDefaultCodexAuth(tempHome);
    const childProcess = await import('child_process');
    const fallbackCodexHome = path.join(tempHome, '.codex');
    let capturedCodexHome: string | undefined;

    vi.mocked(childProcess.spawn).mockImplementation(((
      _cmd: string,
      _args: readonly string[] | undefined,
      opts?: { env?: Record<string, string> },
    ) => {
      capturedCodexHome = opts?.env?.CODEX_HOME;
      return createFakeChildProcess({
        codex: {
          limitName: 'Codex',
          primary: {
            usedPercent: 12.4,
            resetsAt: new Date(Date.now() + 3_600_000).toISOString(),
          },
          secondary: {
            usedPercent: 67.6,
            resetsAt: new Date(Date.now() + 86_400_000).toISOString(),
          },
        },
      }) as never;
    }) as unknown as typeof childProcess.spawn);

    const rotation = await import('./codex-token-rotation.js');
    const usage = await import('./codex-usage-collector.js');

    rotation.initCodexTokenRotation();
    const result = await usage.refreshActiveCodexUsage();

    expect(rotation.getCodexAccountCount()).toBe(1);
    expect(capturedCodexHome).toBe(fallbackCodexHome);
    expect(result.fetchedAt).toEqual(expect.any(String));
    expect(result.rows).toEqual([
      expect.objectContaining({
        name: 'Codex',
        h5pct: 12,
        d7pct: 68,
      }),
    ]);
  });

  it('handles a lone weekly window (secondary: null) without throwing and routes it to 7d', async () => {
    createDefaultCodexAuth(tempHome);
    const childProcess = await import('child_process');

    vi.mocked(childProcess.spawn).mockImplementation(((
      _cmd: string,
      _args: readonly string[] | undefined,
      _opts?: { env?: Record<string, string> },
    ) => {
      // Some plans (e.g. Plus) report only a weekly window in `primary` with
      // `secondary: null`. This previously threw on `secondary.usedPercent`.
      return createFakeChildProcess({
        codex: {
          limitName: 'Codex',
          primary: {
            usedPercent: 10,
            windowDurationMins: 10080, // 7 days
            resetsAt: new Date(Date.now() + 4 * 86_400_000).toISOString(),
          },
          secondary: null,
        },
      }) as never;
    }) as unknown as typeof childProcess.spawn);

    const rotation = await import('./codex-token-rotation.js');
    const usage = await import('./codex-usage-collector.js');

    rotation.initCodexTokenRotation();
    const result = await usage.refreshActiveCodexUsage();

    // Usage must be applied (not swallowed by a TypeError), and the lone weekly
    // window is shown as 7d while the missing 5h window renders as unknown.
    expect(result.fetchedAt).toEqual(expect.any(String));
    expect(result.rows).toEqual([
      expect.objectContaining({
        name: 'Codex',
        h5pct: -1,
        d7pct: 10,
      }),
    ]);
  });

  it('finds codex via ~/.hermes/node/bin when running under bun', async () => {
    createDefaultCodexAuth(tempHome);
    const childProcess = await import('child_process');
    const bunExecPath = path.join(tempHome, '.bun', 'bin', 'bun');
    const hermesBin = path.join(tempHome, '.hermes', 'node', 'bin');
    const originalExecPath = process.execPath;
    const originalPath = process.env.PATH;

    fs.mkdirSync(hermesBin, { recursive: true });
    process.env.PATH = '/usr/local/bin:/usr/bin:/bin';
    Object.defineProperty(process, 'execPath', {
      configurable: true,
      writable: true,
      value: bunExecPath,
    });

    vi.mocked(childProcess.spawn).mockImplementation(((
      cmd: string,
      _args: readonly string[] | undefined,
      opts?: { env?: Record<string, string> },
    ) => {
      const spawnPathEntries = (opts?.env?.PATH || '').split(path.delimiter);
      expect(cmd).toBe('codex');
      expect(spawnPathEntries).toContain(path.dirname(bunExecPath));
      expect(spawnPathEntries).toContain(hermesBin);

      if (!spawnPathEntries.includes(hermesBin)) {
        const proc = new EventEmitter() as EventEmitter & {
          stdout: EventEmitter;
          stdin: { write: ReturnType<typeof vi.fn> };
          kill: ReturnType<typeof vi.fn>;
        };
        proc.stdout = new EventEmitter();
        proc.stdin = { write: vi.fn() };
        proc.kill = vi.fn();
        setImmediate(() => proc.emit('error', new Error('spawn codex ENOENT')));
        return proc as never;
      }

      return createFakeChildProcess({
        codex: {
          limitName: 'Codex',
          primary: {
            usedPercent: 34.2,
            resetsAt: new Date(Date.now() + 7_200_000).toISOString(),
          },
          secondary: {
            usedPercent: 56.1,
            resetsAt: new Date(Date.now() + 172_800_000).toISOString(),
          },
        },
      }) as never;
    }) as unknown as typeof childProcess.spawn);

    try {
      const rotation = await import('./codex-token-rotation.js');
      const usage = await import('./codex-usage-collector.js');

      rotation.initCodexTokenRotation();
      const result = await usage.refreshActiveCodexUsage();

      expect(result.fetchedAt).toEqual(expect.any(String));
      expect(result.rows).toEqual([
        expect.objectContaining({
          name: 'Codex',
          h5pct: 34,
          d7pct: 56,
        }),
      ]);
    } finally {
      Object.defineProperty(process, 'execPath', {
        configurable: true,
        writable: true,
        value: originalExecPath,
      });
      if (originalPath === undefined) {
        delete process.env.PATH;
      } else {
        process.env.PATH = originalPath;
      }
    }
  });
});
