import { test, describe, beforeEach, afterEach } from 'node:test';
import assert from 'node:assert/strict';
import { getUsage, clearCache } from '../dist/usage-api.js';
import { mkdtemp, rm, mkdir, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';

let tempHome = null;

async function createTempHome() {
  return await mkdtemp(path.join(tmpdir(), 'claude-hud-usage-'));
}

async function writeCredentials(homeDir, credentials) {
  const credDir = path.join(homeDir, '.claude');
  await mkdir(credDir, { recursive: true });
  await writeFile(path.join(credDir, '.credentials.json'), JSON.stringify(credentials), 'utf8');
}

function buildCredentials(overrides = {}) {
  return {
    claudeAiOauth: {
      accessToken: 'test-token',
      subscriptionType: 'claude_pro_2024',
      expiresAt: Date.now() + 3600000, // 1 hour from now
      ...overrides,
    },
  };
}

function buildApiResponse(overrides = {}) {
  return {
    five_hour: {
      utilization: 25,
      resets_at: '2026-01-06T15:00:00Z',
    },
    seven_day: {
      utilization: 10,
      resets_at: '2026-01-13T00:00:00Z',
    },
    ...overrides,
  };
}

describe('getUsage', () => {
  beforeEach(async () => {
    tempHome = await createTempHome();
    clearCache(tempHome);
  });

  afterEach(async () => {
    if (tempHome) {
      await rm(tempHome, { recursive: true, force: true });
      tempHome = null;
    }
  });

  test('returns null when credentials file does not exist', async () => {
    let fetchCalls = 0;
    const result = await getUsage({
      homeDir: () => tempHome,
      fetchApi: async () => {
        fetchCalls += 1;
        return null;
      },
      now: () => 1000,
      readKeychain: () => null, // Disable Keychain for tests
    });

    assert.equal(result, null);
    assert.equal(fetchCalls, 0);
  });

  test('returns null when claudeAiOauth is missing', async () => {
    await writeCredentials(tempHome, {});
    let fetchCalls = 0;
    const result = await getUsage({
      homeDir: () => tempHome,
      fetchApi: async () => {
        fetchCalls += 1;
        return buildApiResponse();
      },
      now: () => 1000,
      readKeychain: () => null,
    });

    assert.equal(result, null);
    assert.equal(fetchCalls, 0);
  });

  test('returns null when token is expired', async () => {
    await writeCredentials(tempHome, buildCredentials({ expiresAt: 500 }));
    let fetchCalls = 0;
    const result = await getUsage({
      homeDir: () => tempHome,
      fetchApi: async () => {
        fetchCalls += 1;
        return buildApiResponse();
      },
      now: () => 1000,
      readKeychain: () => null,
    });

    assert.equal(result, null);
    assert.equal(fetchCalls, 0);
  });

  test('returns null for API users (no subscriptionType)', async () => {
    await writeCredentials(tempHome, buildCredentials({ subscriptionType: 'api' }));
    let fetchCalls = 0;
    const result = await getUsage({
      homeDir: () => tempHome,
      fetchApi: async () => {
        fetchCalls += 1;
        return buildApiResponse();
      },
      now: () => 1000,
      readKeychain: () => null,
    });

    assert.equal(result, null);
    assert.equal(fetchCalls, 0);
  });

  test('uses complete keychain credentials without falling back to file', async () => {
    // No file credentials - keychain should be sufficient
    let usedToken = null;
    const result = await getUsage({
      homeDir: () => tempHome,
      fetchApi: async (token) => {
        usedToken = token;
        return buildApiResponse();
      },
      now: () => 1000,
      readKeychain: () => ({ accessToken: 'keychain-token', subscriptionType: 'claude_max_2024' }),
    });

    assert.equal(usedToken, 'keychain-token');
    assert.equal(result?.planName, 'Max');
  });

  test('uses keychain token with file subscriptionType when keychain lacks subscriptionType', async () => {
    await writeCredentials(tempHome, buildCredentials({
      accessToken: 'old-file-token',
      subscriptionType: 'claude_pro_2024',
    }));
    let usedToken = null;
    const result = await getUsage({
      homeDir: () => tempHome,
      fetchApi: async (token) => {
        usedToken = token;
        return buildApiResponse();
      },
      now: () => 1000,
      readKeychain: () => ({ accessToken: 'keychain-token', subscriptionType: '' }),
    });

    // Must use keychain token (authoritative), but can use file's subscriptionType
    assert.equal(usedToken, 'keychain-token', 'should use keychain token, not file token');
    assert.equal(result?.planName, 'Pro');
  });

  test('returns null when keychain has token but no subscriptionType anywhere', async () => {
    // No file credentials, keychain has no subscriptionType
    // This user is treated as an API user (no usage limits)
    let fetchCalls = 0;
    const result = await getUsage({
      homeDir: () => tempHome,
      fetchApi: async () => {
        fetchCalls += 1;
        return buildApiResponse();
      },
      now: () => 1000,
      readKeychain: () => ({ accessToken: 'keychain-token', subscriptionType: '' }),
    });

    // No subscriptionType means API user, returns null without calling API
    assert.equal(result, null);
    assert.equal(fetchCalls, 0);
  });

  test('parses plan name and usage data', async () => {
    await writeCredentials(tempHome, buildCredentials({ subscriptionType: 'claude_pro_2024' }));
    let fetchCalls = 0;
    const result = await getUsage({
      homeDir: () => tempHome,
      fetchApi: async () => {
        fetchCalls += 1;
        return buildApiResponse();
      },
      now: () => 1000,
      readKeychain: () => null,
    });

    assert.equal(fetchCalls, 1);
    assert.equal(result?.planName, 'Pro');
    assert.equal(result?.fiveHour, 25);
    assert.equal(result?.sevenDay, 10);
  });

  test('parses Team plan name', async () => {
    await writeCredentials(tempHome, buildCredentials({ subscriptionType: 'claude_team_2024' }));
    const result = await getUsage({
      homeDir: () => tempHome,
      fetchApi: async () => buildApiResponse(),
      now: () => 1000,
      readKeychain: () => null,
    });

    assert.equal(result?.planName, 'Team');
  });

  test('returns apiUnavailable and caches failures', async () => {
    await writeCredentials(tempHome, buildCredentials());
    let fetchCalls = 0;
    let nowValue = 1000;
    const fetchApi = async () => {
      fetchCalls += 1;
      return null;
    };

    const first = await getUsage({
      homeDir: () => tempHome,
      fetchApi,
      now: () => nowValue,
      readKeychain: () => null,
    });
    assert.equal(first?.apiUnavailable, true);
    assert.equal(fetchCalls, 1);

    nowValue += 10_000;
    const cached = await getUsage({
      homeDir: () => tempHome,
      fetchApi,
      now: () => nowValue,
      readKeychain: () => null,
    });
    assert.equal(cached?.apiUnavailable, true);
    assert.equal(fetchCalls, 1);

    nowValue += 6_000;
    const second = await getUsage({
      homeDir: () => tempHome,
      fetchApi,
      now: () => nowValue,
      readKeychain: () => null,
    });
    assert.equal(second?.apiUnavailable, true);
    assert.equal(fetchCalls, 2);
  });
});

describe('getUsage caching behavior', () => {
  beforeEach(async () => {
    tempHome = await createTempHome();
    clearCache(tempHome);
  });

  afterEach(async () => {
    if (tempHome) {
      await rm(tempHome, { recursive: true, force: true });
      tempHome = null;
    }
  });

  test('cache expires after 60 seconds for success', async () => {
    await writeCredentials(tempHome, buildCredentials());
    let fetchCalls = 0;
    let nowValue = 1000;
    const fetchApi = async () => {
      fetchCalls += 1;
      return buildApiResponse();
    };

    await getUsage({ homeDir: () => tempHome, fetchApi, now: () => nowValue, readKeychain: () => null });
    assert.equal(fetchCalls, 1);

    nowValue += 30_000;
    await getUsage({ homeDir: () => tempHome, fetchApi, now: () => nowValue, readKeychain: () => null });
    assert.equal(fetchCalls, 1);

    nowValue += 31_000;
    await getUsage({ homeDir: () => tempHome, fetchApi, now: () => nowValue, readKeychain: () => null });
    assert.equal(fetchCalls, 2);
  });

  test('cache expires after 15 seconds for failures', async () => {
    await writeCredentials(tempHome, buildCredentials());
    let fetchCalls = 0;
    let nowValue = 1000;
    const fetchApi = async () => {
      fetchCalls += 1;
      return null;
    };

    await getUsage({ homeDir: () => tempHome, fetchApi, now: () => nowValue, readKeychain: () => null });
    assert.equal(fetchCalls, 1);

    nowValue += 10_000;
    await getUsage({ homeDir: () => tempHome, fetchApi, now: () => nowValue, readKeychain: () => null });
    assert.equal(fetchCalls, 1);

    nowValue += 6_000;
    await getUsage({ homeDir: () => tempHome, fetchApi, now: () => nowValue, readKeychain: () => null });
    assert.equal(fetchCalls, 2);
  });

  test('clearCache removes file-based cache', async () => {
    await writeCredentials(tempHome, buildCredentials());
    let fetchCalls = 0;
    const fetchApi = async () => {
      fetchCalls += 1;
      return buildApiResponse();
    };

    await getUsage({ homeDir: () => tempHome, fetchApi, now: () => 1000, readKeychain: () => null });
    assert.equal(fetchCalls, 1);

    clearCache(tempHome);
    await getUsage({ homeDir: () => tempHome, fetchApi, now: () => 2000, readKeychain: () => null });
    assert.equal(fetchCalls, 2);
  });
});

describe('isLimitReached', () => {
  test('returns true when fiveHour is 100', async () => {
    // Import from types since isLimitReached is exported there
    const { isLimitReached } = await import('../dist/types.js');

    const data = {
      planName: 'Pro',
      fiveHour: 100,
      sevenDay: 50,
      fiveHourResetAt: null,
      sevenDayResetAt: null,
    };

    assert.equal(isLimitReached(data), true);
  });

  test('returns true when sevenDay is 100', async () => {
    const { isLimitReached } = await import('../dist/types.js');

    const data = {
      planName: 'Pro',
      fiveHour: 50,
      sevenDay: 100,
      fiveHourResetAt: null,
      sevenDayResetAt: null,
    };

    assert.equal(isLimitReached(data), true);
  });

  test('returns false when both are below 100', async () => {
    const { isLimitReached } = await import('../dist/types.js');

    const data = {
      planName: 'Pro',
      fiveHour: 50,
      sevenDay: 50,
      fiveHourResetAt: null,
      sevenDayResetAt: null,
    };

    assert.equal(isLimitReached(data), false);
  });

  test('handles null values correctly', async () => {
    const { isLimitReached } = await import('../dist/types.js');

    const data = {
      planName: 'Pro',
      fiveHour: null,
      sevenDay: null,
      fiveHourResetAt: null,
      sevenDayResetAt: null,
    };

    // null !== 100, so should return false
    assert.equal(isLimitReached(data), false);
  });

  test('returns true when sevenDay is 100 but fiveHour is null', async () => {
    const { isLimitReached } = await import('../dist/types.js');

    const data = {
      planName: 'Pro',
      fiveHour: null,
      sevenDay: 100,
      fiveHourResetAt: null,
      sevenDayResetAt: null,
    };

    assert.equal(isLimitReached(data), true);
  });
});
