import { describe, it, beforeEach } from 'node:test';
import assert from 'node:assert/strict';
import {
  extractUsage,
  recordUsage,
  getRunUsage,
  resetRunUsage,
  recordRateLimit,
  getRateLimit,
  resetRateLimit,
  resolveRemainingPercent,
  fetchAccountQuota,
  formatUsageStats,
  formatUsageStatsLine,
} from '../usage.js';

describe('extractUsage', () => {
  it('parses OpenAI-compatible usage', () => {
    const u = extractUsage({ usage: { prompt_tokens: 100, completion_tokens: 20, total_tokens: 120 } });
    assert.deepEqual(u, { promptTokens: 100, completionTokens: 20, totalTokens: 120 });
  });

  it('parses OpenAI Responses input/output tokens and derives total', () => {
    const u = extractUsage({ usage: { input_tokens: 50, output_tokens: 10 } });
    assert.deepEqual(u, { promptTokens: 50, completionTokens: 10, totalTokens: 60 });
  });

  it('parses Gemini usageMetadata', () => {
    const u = extractUsage({ usageMetadata: { promptTokenCount: 30, candidatesTokenCount: 5, totalTokenCount: 35 } });
    assert.deepEqual(u, { promptTokens: 30, completionTokens: 5, totalTokens: 35 });
  });

  it('parses Ollama native eval counts', () => {
    const u = extractUsage({ prompt_eval_count: 12, eval_count: 8 });
    assert.deepEqual(u, { promptTokens: 12, completionTokens: 8, totalTokens: 20 });
  });

  it('parses OpenCode tokens from info.tokens', () => {
    const u = extractUsage({ info: { tokens: { input: 7, output: 3 } } });
    assert.deepEqual(u, { promptTokens: 7, completionTokens: 3, totalTokens: 10 });
  });

  it('respects an explicit total_tokens of 0 instead of summing', () => {
    const u = extractUsage({ usage: { prompt_tokens: 10, completion_tokens: 2, total_tokens: 0 } });
    assert.equal(u.totalTokens, 0);
  });

  it('returns null when no usage info is present', () => {
    assert.equal(extractUsage({ choices: [{ message: { content: 'hi' } }] }), null);
    assert.equal(extractUsage(null), null);
  });

  it('handles malformed usage payloads without throwing or NaN', () => {
    assert.equal(extractUsage(undefined), null);
    assert.equal(extractUsage('not-an-object'), null);
    assert.equal(extractUsage({ usage: 'x' }), null);   // usage 非物件
    assert.equal(extractUsage({ usage: {} }), null);     // 欄位缺失
    // 非數字 token 欄位 → 一律以 0 計，最終無有效 usage → null（不會回傳 NaN）
    assert.equal(extractUsage({ usage: { prompt_tokens: 'abc', completion_tokens: null, total_tokens: 'x' } }), null);
  });
});

describe('recordUsage / getRunUsage', () => {
  beforeEach(() => resetRunUsage());

  it('accumulates across calls and counts every call', () => {
    recordUsage({ usage: { prompt_tokens: 10, completion_tokens: 2, total_tokens: 12 } });
    recordUsage({ usage: { prompt_tokens: 5, completion_tokens: 1, total_tokens: 6 } });
    recordUsage({ parts: [] }); // no usage → still counts as a call
    assert.deepEqual(getRunUsage(), { calls: 3, promptTokens: 15, completionTokens: 3, totalTokens: 18 });
  });

  it('returns a copy, not the internal object', () => {
    const a = getRunUsage();
    a.calls = 999;
    assert.equal(getRunUsage().calls, 0);
  });
});

describe('fetchAccountQuota', () => {
  it('reads OpenRouter credits via injected get', async () => {
    const get = async (url, opts) => {
      assert.match(url, /openrouter\.ai\/api\/v1\/auth\/key$/);
      assert.equal(opts.headers.Authorization, 'Bearer sk-or-xxx');
      assert.equal(opts.httpsAgent, undefined);
      return { data: { data: { usage: 12.4, limit: 100, limit_remaining: 87.6 } } };
    };
    const q = await fetchAccountQuota('openai', { apiKeys: ['sk-or-xxx'], baseURL: 'https://openrouter.ai/api/v1' }, { get });
    assert.deepEqual(q, { available: true, used: 12.4, limit: 100, remaining: 87.6, currency: 'USD', source: 'openrouter' });
  });

  it('derives remaining when OpenRouter omits limit_remaining', async () => {
    const get = async () => ({ data: { data: { usage: 10, limit: 50 } } });
    const q = await fetchAccountQuota('openai', { apiKeys: ['k'], baseURL: 'https://openrouter.ai/api/v1' }, { get });
    assert.equal(q.remaining, 40);
  });

  it('reports unavailable for plain OpenAI (no openrouter)', async () => {
    const q = await fetchAccountQuota('openai', { apiKeys: ['k'], baseURL: 'https://api.openai.com/v1' }, { get: async () => { throw new Error('should not call'); } });
    assert.equal(q.available, false);
    assert.match(q.reason, /API key 無法取得/);
  });

  it('does not treat spoofed openrouter hostnames as OpenRouter (no key leak)', async () => {
    const get = async () => { throw new Error('should not be called for spoofed host'); };
    for (const baseURL of ['https://openrouter.ai.evil.com/api/v1', 'https://evil.com/openrouter.ai']) {
      const q = await fetchAccountQuota('openai', { apiKeys: ['sk-secret'], baseURL }, { get });
      assert.equal(q.available, false);
      assert.match(q.reason, /API key 無法取得/);
    }
  });

  it('only accepts the exact openrouter.ai apex host (subdomains are not OpenRouter)', async () => {
    const get = async () => { throw new Error('should not be called for non-apex host'); };
    const q = await fetchAccountQuota('openai', { apiKeys: ['sk-secret'], baseURL: 'https://api.openrouter.ai/api/v1' }, { get });
    assert.equal(q.available, false);
    assert.match(q.reason, /API key 無法取得/);
  });

  it('reports 不適用 for local platforms', async () => {
    assert.equal((await fetchAccountQuota('ollama', {})).available, false);
    assert.equal((await fetchAccountQuota('opencode', {})).available, false);
    assert.equal((await fetchAccountQuota('antigravity', {})).available, false);
  });

  it('degrades gracefully when the quota call throws', async () => {
    const get = async () => { throw new Error('network down'); };
    const q = await fetchAccountQuota('openai', { apiKeys: ['k'], baseURL: 'https://openrouter.ai/api/v1' }, { get });
    assert.deepEqual(q, { available: false, reason: 'network down' });
  });

  it('reports unsupported provider', async () => {
    const q = await fetchAccountQuota('mystery', {});
    assert.equal(q.available, false);
    assert.match(q.reason, /未支援/);
  });

  it('degrades gracefully when apiKeys is empty or undefined', async () => {
    const get = async () => { throw new Error('should not be called'); };
    // 空陣列 / 未提供 key 都不應丟錯，依平台回報無法取得或不適用
    assert.equal((await fetchAccountQuota('openai', { apiKeys: [], baseURL: 'https://api.openai.com/v1' }, { get })).available, false);
    assert.equal((await fetchAccountQuota('ollama', { apiKeys: [] }, { get })).available, false);
    assert.equal((await fetchAccountQuota('claude', {}, { get })).available, false);
  });
});

describe('recordRateLimit / getRateLimit', () => {
  beforeEach(() => resetRateLimit());

  it('captures OpenAI-style token rate-limit headers (case-insensitive)', () => {
    recordRateLimit({ 'X-RateLimit-Remaining-Tokens': '190000', 'X-RateLimit-Limit-Tokens': '200000' });
    assert.deepEqual(getRateLimit(), { hasData: true, remaining: 190000, limit: 200000, kind: 'tokens' });
  });

  it('captures Anthropic-style token rate-limit headers', () => {
    recordRateLimit({ 'anthropic-ratelimit-tokens-remaining': '8000', 'anthropic-ratelimit-tokens-limit': '10000' });
    assert.deepEqual(getRateLimit(), { hasData: true, remaining: 8000, limit: 10000, kind: 'tokens' });
  });

  it('falls back to request-dimension headers when token headers are absent', () => {
    recordRateLimit({ 'x-ratelimit-remaining-requests': '45', 'x-ratelimit-limit-requests': '60' });
    assert.deepEqual(getRateLimit(), { hasData: true, remaining: 45, limit: 60, kind: 'requests' });
  });

  it('ignores responses without rate-limit headers', () => {
    recordRateLimit({ 'content-type': 'application/json' });
    assert.equal(getRateLimit().hasData, false);
  });
});

describe('resolveRemainingPercent', () => {
  it('prefers account quota when a finite limit exists', () => {
    const pct = resolveRemainingPercent({ available: true, used: 12.4, limit: 100, remaining: 87.6, currency: 'USD' }, { hasData: true, remaining: 1, limit: 10, kind: 'tokens' });
    assert.equal(pct.percent, 87.6);
    assert.equal(pct.basis, '帳號額度');
  });

  it('derives remaining from used when quota.remaining is absent', () => {
    const pct = resolveRemainingPercent({ available: true, used: 25, limit: 100, currency: 'USD' }, null);
    assert.equal(pct.percent, 75);
  });

  it('falls back to rate-limit window percent when quota has no limit', () => {
    const pct = resolveRemainingPercent({ available: false, reason: 'x' }, { hasData: true, remaining: 150000, limit: 200000, kind: 'tokens' });
    assert.equal(pct.percent, 75);
    assert.match(pct.basis, /速率配額（當前視窗，token）/);
  });

  it('returns null percent with a reason when nothing is available', () => {
    const pct = resolveRemainingPercent({ available: false, reason: '本地服務，無帳號額度概念' }, { hasData: false });
    assert.equal(pct.percent, null);
    assert.equal(pct.reason, '本地服務，無帳號額度概念');
  });

  it('explains an unlimited account cannot yield a percent', () => {
    const pct = resolveRemainingPercent({ available: true, used: 5, limit: null }, { hasData: false });
    assert.equal(pct.percent, null);
    assert.match(pct.reason, /無上限/);
  });

  it('returns null percent for non-finite or non-positive quota limits', () => {
    for (const limit of [0, -5, Infinity, NaN, undefined]) {
      const pct = resolveRemainingPercent({ available: true, used: 0, limit, remaining: limit, currency: 'USD' }, null);
      assert.equal(pct.percent, null, `quota.limit=${limit} 應算不出百分比`);
    }
  });

  it('returns null percent for non-finite or non-positive rate limits', () => {
    for (const limit of [0, -1, Infinity, NaN]) {
      const pct = resolveRemainingPercent({ available: false, reason: 'x' }, { hasData: true, remaining: limit, limit, kind: 'tokens' });
      assert.equal(pct.percent, null, `rate.limit=${limit} 應算不出百分比`);
    }
  });

  it('returns null percent when rate.remaining is null/undefined', () => {
    for (const remaining of [null, undefined]) {
      const pct = resolveRemainingPercent({ available: false, reason: 'x' }, { hasData: true, remaining, limit: 200000, kind: 'tokens' });
      assert.equal(pct.percent, null, `rate.remaining=${remaining} 應算不出百分比`);
    }
  });

  it('returns null percent when limit is finite but remaining is non-finite', () => {
    const pct = resolveRemainingPercent({ available: true, used: 0, limit: 100, remaining: Infinity, currency: 'USD' }, null);
    assert.equal(pct.percent, null);
  });

  it('does not divide by zero when quota.limit is 0', () => {
    const pct = resolveRemainingPercent({ available: true, used: 5, limit: 0, currency: 'USD' }, { hasData: false });
    assert.equal(pct.percent, null); // limit > 0 守衛擋掉除以零
    assert.ok(typeof pct.reason === 'string' && pct.reason.length > 0);
  });

  it('reports 100% when remaining equals limit and 0% when remaining is 0', () => {
    const full = resolveRemainingPercent({ available: true, used: 0, limit: 100, remaining: 100, currency: 'USD' }, null);
    assert.equal(full.percent, 100);
    const empty = resolveRemainingPercent({ available: true, used: 100, limit: 100, remaining: 0, currency: 'USD' }, null);
    assert.equal(empty.percent, 0);
  });
});

describe('formatUsageStats', () => {
  const usage = { calls: 7, promptTokens: 18432, completionTokens: 2107, totalTokens: 20539 };

  it('renders token table and remaining percent from account quota', () => {
    const out = formatUsageStats('openai', 'gpt-4o-mini', usage, { available: true, used: 12.4, limit: 100, remaining: 87.6, currency: 'USD' }, null);
    assert.match(out, /## 🤖 AI 助理使用量/);
    assert.match(out, /18,432 \| 2,107 \| 20,539/);
    assert.match(out, /共 7 次呼叫/);
    assert.match(out, /剩餘可用 \*\*87.6%\*\*（帳號額度：USD 87.6 \/ USD 100）/);
  });

  it('renders remaining percent from rate-limit window when quota is unavailable', () => {
    const out = formatUsageStats('claude', 'sonnet', usage, { available: false, reason: 'r' }, { hasData: true, remaining: 150000, limit: 200000, kind: 'tokens' });
    assert.match(out, /剩餘可用 \*\*75%\*\*（速率配額（當前視窗，token）：150,000 \/ 200,000）/);
  });

  it('explains when no percentage can be computed', () => {
    const out = formatUsageStats('ollama', 'llama3', usage, { available: false, reason: '本地服務，無帳號額度概念' }, { hasData: false });
    assert.match(out, /剩餘可用：無法計算百分比（本地服務，無帳號額度概念）/);
  });
});

describe('formatUsageStatsLine', () => {
  const usage = { calls: 3, promptTokens: 100, completionTokens: 20, totalTokens: 120 };

  it('summarises tokens and remaining percent on one line', () => {
    const line = formatUsageStatsLine('openai', 'gpt-4o-mini', usage, { available: true, used: 1, limit: 10, remaining: 9, currency: 'USD' }, null);
    assert.equal(line, '本次 openai/gpt-4o-mini: 提示100 + 回應20 = 120 token（3 次呼叫）；剩餘可用: 90%（帳號額度 USD 9/USD 10）');
  });

  it('notes when remaining percent cannot be computed', () => {
    const line = formatUsageStatsLine('ollama', 'llama3', usage, { available: false, reason: '本地服務，無帳號額度概念' }, { hasData: false });
    assert.match(line, /；剩餘可用: 無法計算（本地服務，無帳號額度概念）/);
  });

  it('produces safe text (no NaN) when quota/rate carry invalid numbers', () => {
    // quota.limit 為 NaN、rate.limit 為 NaN → 不應算出百分比、不得輸出 NaN
    const line = formatUsageStatsLine('openai', 'm', usage,
      { available: true, used: 5, limit: NaN, currency: 'USD' },
      { hasData: true, remaining: NaN, limit: NaN, kind: 'tokens' });
    assert.doesNotMatch(line, /NaN/);
    assert.match(line, /剩餘可用: 無法計算/);
  });

  it('does not output Infinity/NaN/negative percent for invalid quota numbers', () => {
    const ownUsage = { calls: 1, promptTokens: 1, completionTokens: 1, totalTokens: 2 };
    for (const limit of [Infinity, 0, -5, NaN]) {
      const line = formatUsageStatsLine('openai', 'm', ownUsage, { available: true, used: 0, limit, remaining: limit, currency: 'USD' }, null);
      assert.doesNotMatch(line, /NaN|Infinity|-\d+%/);
      assert.match(line, /剩餘可用: 無法計算/);
    }
  });

  it('falls back to a valid rate percent when only the quota limit is invalid', () => {
    const ownUsage = { calls: 1, promptTokens: 1, completionTokens: 1, totalTokens: 2 };
    const line = formatUsageStatsLine('openai', 'm', ownUsage,
      { available: true, used: 0, limit: Infinity, currency: 'USD' },          // quota 無效
      { hasData: true, remaining: 150000, limit: 200000, kind: 'tokens' });     // rate 有效 → 75%
    assert.match(line, /剩餘可用: 75%（速率配額/);
  });
});
