import assert from 'node:assert/strict';
import { describe, it } from 'node:test';
import {
  PLAN_CATALOG,
  createSubscriptionService,
  getPlanDef,
  tokensToCallsApprox,
} from './billing-subscription.mjs';

describe('PLAN_CATALOG', () => {
  it('has expected plan types', () => {
    assert.ok(PLAN_CATALOG.free);
    assert.ok(PLAN_CATALOG.lite);
    assert.ok(PLAN_CATALOG.standard);
    assert.ok(PLAN_CATALOG.pro);
  });

  it('free plan has positive token limit', () => {
    assert.ok(PLAN_CATALOG.free.periodTokens > 0);
    assert.equal(PLAN_CATALOG.free.priceCents, 0);
  });

  it('pro plan has unlimited tokens (0)', () => {
    assert.equal(PLAN_CATALOG.pro.periodTokens, 0);
  });

  it('all plans have valid overageRate', () => {
    for (const [, plan] of Object.entries(PLAN_CATALOG)) {
      assert.ok(plan.overageRate > 0 && plan.overageRate <= 1.0, `overageRate out of range: ${plan.overageRate}`);
    }
  });
});

describe('getPlanDef', () => {
  it('returns plan for known type', () => {
    assert.deepEqual(getPlanDef('standard'), PLAN_CATALOG.standard);
  });

  it('returns null for unknown type', () => {
    assert.equal(getPlanDef('enterprise'), null);
  });
});

describe('tokensToCallsApprox', () => {
  it('returns null for unlimited (0)', () => {
    assert.equal(tokensToCallsApprox(0), null);
  });

  it('calculates approximate calls', () => {
    assert.equal(tokensToCallsApprox(150_000), 50);   // free ~50 calls
    assert.equal(tokensToCallsApprox(1_200_000), 400); // lite ~400 calls
  });
});

// Mock pool helper
function makePool(subRow = null) {
  const queries = [];
  const conn = {
    queries: [],
    async query(sql, params) {
      this.queries.push({ sql, params });
      if (sql.includes('FOR UPDATE') && subRow) {
        return [[subRow]];
      }
      if (sql.includes('UPDATE h5_subscriptions')) {
        return [{ affectedRows: 1 }];
      }
      return [[subRow].filter(Boolean)];
    },
    async beginTransaction() {},
    async commit() {},
    async rollback() {},
    release() {},
  };

  return {
    queries,
    async query(sql, params) {
      queries.push({ sql, params });
      if (sql.includes('SELECT * FROM h5_subscriptions') && subRow) return [[subRow]];
      if (sql.includes('COUNT(*)')) return [[{ total: subRow ? 1 : 0 }]];
      if (sql.includes('UPDATE h5_subscriptions') || sql.includes('UPDATE h5_users')) return [{ affectedRows: 1 }];
      if (sql.includes('INSERT INTO h5_subscriptions')) return [{ insertId: 1 }];
      return [subRow ? [subRow] : []];
    },
    async getConnection() { return conn; },
    _conn: conn,
  };
}

function makeSubRow(overrides = {}) {
  const now = Date.now();
  return {
    id: 'sub-1',
    user_id: 'user-1',
    plan_type: 'standard',
    status: 'active',
    period_tokens_limit: 4_500_000,
    period_tokens_used: 0,
    period_images_limit: 50,
    period_images_used: 0,
    period_start: now,
    period_end: now + 30 * 24 * 60 * 60 * 1000,
    expires_at: now + 30 * 24 * 60 * 60 * 1000,
    overage_rate: 0.70,
    operator_id: null,
    note: null,
    created_at: now,
    updated_at: now,
    ...overrides,
  };
}

describe('createSubscriptionService', () => {
  describe('getActiveSubscription', () => {
    it('returns null when no active subscription', async () => {
      const pool = makePool(null);
      const svc = createSubscriptionService(pool);
      const result = await svc.getActiveSubscription('user-1');
      assert.equal(result, null);
    });

    it('returns mapped subscription when active', async () => {
      const pool = makePool(makeSubRow());
      const svc = createSubscriptionService(pool);
      const result = await svc.getActiveSubscription('user-1');
      assert.ok(result);
      assert.equal(result.planType, 'standard');
      assert.equal(result.periodTokensLimit, 4_500_000);
      assert.equal(result.overageRate, 0.70);
    });
  });

  describe('consumeQuota', () => {
    it('returns fullyCovers=true for unlimited plan (limit=0)', async () => {
      const subRow = makeSubRow({ plan_type: 'pro', period_tokens_limit: 0, period_tokens_used: 0 });
      const pool = makePool(subRow);
      const svc = createSubscriptionService(pool);
      const coverage = await svc.consumeQuota('user-1', 5000, pool._conn);
      assert.equal(coverage.fullyCovers, true);
    });

    it('returns fullyCovers=true when quota has enough tokens', async () => {
      const subRow = makeSubRow({ period_tokens_limit: 4_500_000, period_tokens_used: 0 });
      const pool = makePool(subRow);
      const svc = createSubscriptionService(pool);
      const coverage = await svc.consumeQuota('user-1', 2000, pool._conn);
      assert.equal(coverage.fullyCovers, true);
    });

    it('returns fullyCovers=false when quota exhausted', async () => {
      const subRow = makeSubRow({ period_tokens_limit: 4_500_000, period_tokens_used: 4_500_000 });
      const pool = makePool(subRow);
      const svc = createSubscriptionService(pool);
      const coverage = await svc.consumeQuota('user-1', 2000, pool._conn);
      assert.equal(coverage.fullyCovers, false);
      assert.equal(coverage.overageRate, 0.70);
    });

    it('returns fullyCovers=false and overageRate=1.0 when no subscription', async () => {
      const pool = makePool(null);
      const svc = createSubscriptionService(pool);
      const coverage = await svc.consumeQuota('user-1', 2000, pool._conn);
      assert.equal(coverage.fullyCovers, false);
      assert.equal(coverage.overageRate, 1.0);
    });

    it('returns fullyCovers=true for zero delta tokens', async () => {
      const pool = makePool(null);
      const svc = createSubscriptionService(pool);
      const coverage = await svc.consumeQuota('user-1', 0, pool._conn);
      assert.equal(coverage.fullyCovers, true);
    });
  });

  describe('consumeImageQuota', () => {
    it('deducts one image via its own transaction when quota exists', async () => {
      const subRow = makeSubRow({ period_images_limit: 50, period_images_used: 0 });
      const pool = makePool(subRow);
      const svc = createSubscriptionService(pool);
      const coverage = await svc.consumeImageQuota('user-1', 1);

      assert.equal(coverage.fullyCovers, true);
      assert.ok(
        pool._conn.queries.some(
          ({ sql, params }) =>
            sql.includes('SET period_images_used = period_images_used + ?') &&
            params?.[0] === 1,
        ),
      );
    });
  });

  describe('cancelSubscription', () => {
    it('returns cancelled=true when active sub exists', async () => {
      const pool = makePool(makeSubRow());
      const svc = createSubscriptionService(pool);
      const result = await svc.cancelSubscription('user-1');
      assert.equal(result.ok, true);
      assert.equal(result.cancelled, true);
    });
  });

  describe('grantSubscription', () => {
    it('returns error for unknown plan type', async () => {
      const pool = makePool(null);
      const svc = createSubscriptionService(pool);
      const result = await svc.grantSubscription('user-1', 'enterprise', 30);
      assert.equal(result.ok, false);
      assert.match(result.message, /未知套餐/);
    });
  });
});
