import test from 'node:test';
import assert from 'node:assert/strict';
import { closeDb, getDb, resetDbFile, setSetting, withTransaction } from '../src/db.js';
import {
  buyVipWithPoints,
  claimQuest,
  convertPoints,
  getGameState,
  getOrCreateUser,
  listQuests,
  spinWheel,
  tap
} from '../src/game.js';
import { getUserBalances, postTransferInTx, reconcileLedger, userAccountId } from '../src/ledger.js';

function expectCode(fn, code) {
  assert.throws(fn, (error) => error?.code === code);
}

test('OnlyCoin core economy is server-authoritative and idempotent', () => {
  resetDbFile();
  const db = getDb();
  setSetting(db, 'conversion_unlock_at_launch', false);
  setSetting(db, 'conversion_enabled', true);
  setSetting(db, 'conversion_rate_bps', 1000);
  setSetting(db, 'referral_qualifying_taps', 100);
  setSetting(db, 'referral_reward_points', 1000);
  setSetting(db, 'wheel_daily_free_spins', 1);

  const alice = getOrCreateUser(db, { externalId:'test:alice', username:'Alice', displayName:'Alice' });
  const aliceState = getGameState(db, alice.id);
  assert.equal(aliceState.balances.OC_POINTS, 0);
  assert.equal(aliceState.balances.OC_TOKEN, 0);

  const firstTap = tap(db, alice.id, 10, 'test:tap:alice:1');
  const duplicateTap = tap(db, alice.id, 10, 'test:tap:alice:1');
  assert.equal(firstTap.transactionId, duplicateTap.transactionId);
  assert.equal(duplicateTap.duplicate, true);
  assert.equal(getGameState(db, alice.id).state.tapTotal, 10);

  const bob = getOrCreateUser(db, { externalId:'test:bob', username:'Bob', displayName:'Bob', referralCode:alice.referral_code });
  tap(db, bob.id, 50, 'test:tap:bob:1');
  const qualification = tap(db, bob.id, 50, 'test:tap:bob:2');
  assert.equal(qualification.referralRewards.length, 1);
  assert.equal(qualification.referralRewards[0].userId, alice.id);
  assert.equal(qualification.referralRewards[0].amount, 1000);

  tap(db, alice.id, 50, 'test:tap:alice:2');
  tap(db, alice.id, 40, 'test:tap:alice:3');
  const tapQuest = listQuests(db, alice.id).find((quest) => quest.id === 'tap-100');
  assert.equal(tapQuest.completed, true);
  const questClaim = claimQuest(db, alice.id, 'tap-100', 'test:quest:alice:tap');
  const duplicateClaim = claimQuest(db, alice.id, 'tap-100', 'test:quest:alice:tap');
  assert.equal(duplicateClaim.duplicate, true);
  assert.equal(questClaim.rewardPoints, 150);
  expectCode(() => claimQuest(db, alice.id, 'tap-100', 'test:quest:alice:tap:second-key'), 'QUEST_ALREADY_CLAIMED');

  // Keep the wheel reward deterministic so a random VIP trial cannot add another
  // daily free-spin entitlement and make this idempotency assertion flaky.
  db.exec(`UPDATE wheel_segments SET enabled=0; UPDATE wheel_segments SET enabled=1,weight=1 WHERE id='points-750';`);
  const spin = spinWheel(db, alice.id, 'free', 'test:wheel:alice:1');
  const sameSpin = spinWheel(db, alice.id, 'free', 'test:wheel:alice:1');
  assert.equal(spin.spinId, sameSpin.spinId);
  expectCode(() => spinWheel(db, alice.id, 'free', 'test:wheel:alice:2'), 'SPIN_IN_PROGRESS');
  db.prepare('UPDATE user_game_state SET last_wheel_spin_at=0 WHERE user_id=?').run(alice.id);
  expectCode(() => spinWheel(db, alice.id, 'free', 'test:wheel:alice:2-after-cooldown'), 'FREE_SPIN_USED');

  withTransaction(db, () => {
    postTransferInTx(db, {
      fromAccountId:'sys-token-issuance',
      toAccountId:'sys-token-treasury',
      amount:100000,
      type:'test_treasury_funding',
      ref:'test-conversion-treasury',
      idempotencyKey:'test:fund:token:treasury'
    });
    postTransferInTx(db, {
      fromAccountId:'sys-points-admin',
      toAccountId:userAccountId(alice.id, 'OC_POINTS'),
      amount:2000,
      type:'admin_grant',
      ref:'test-conversion-funding',
      idempotencyKey:'test:grant:alice:conversion'
    });
  });
  const beforeConversion = getUserBalances(db, alice.id);
  const conversion = convertPoints(db, alice.id, 1000, 'test:convert:alice:1');
  assert.equal(conversion.tokensReceived, 100);
  const sameConversion = convertPoints(db, alice.id, 1000, 'test:convert:alice:1');
  assert.equal(sameConversion.duplicate, true);
  const afterConversion = getUserBalances(db, alice.id);
  assert.equal(afterConversion.OC_TOKEN, beforeConversion.OC_TOKEN + 100);
  assert.equal(afterConversion.OC_POINTS, beforeConversion.OC_POINTS - 1000);

  withTransaction(db, () => {
    postTransferInTx(db, {
      fromAccountId:'sys-points-admin',
      toAccountId:userAccountId(alice.id, 'OC_POINTS'),
      amount:30000,
      type:'admin_grant',
      ref:'test-vip-funding',
      idempotencyKey:'test:grant:alice:vip'
    });
  });
  const vip = buyVipWithPoints(db, alice.id, 1, 'test:vip:alice:1');
  assert.equal(vip.purchasedTier, 1);
  assert.equal(getGameState(db, alice.id).state.vipTier, 1);

  const reconciliation = reconcileLedger(db);
  assert.equal(reconciliation.ok, true, JSON.stringify(reconciliation));
  closeDb();
});
