/**
 * Quest depth tests (T011c): chains, all-quests streak bonus, weekly challenges,
 * and engagement-state persistence.
 */
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import {
  disableDatabaseStorage,
  clearAllQuests,
  clearPlayerQuests,
  // chains
  getQuestChainStep,
  unlockQuestChain,
  isChainUnlocked,
  updateChainProgress,
  // streak
  recordFullClear,
  getFullClearStreak,
  STREAK_BONUS_THRESHOLD,
  STREAK_BONUS_REWARD,
  // weekly
  getWeeklyChallengeTemplates,
  getOrRollWeeklyChallenge,
  updateWeeklyChallengeProgress,
  // persistence
  loadQuestEngagement,
  serializeQuestEngagement,
} from '../../../src/services/retention/dailyQuests.js';

interface TestPlayer {
  userId: string;
  c: { diamonds: number };
}

function makePlayer(id = 'p1'): TestPlayer {
  return { userId: id, c: { diamonds: 0 } };
}

describe('Quest depth', () => {
  beforeEach(() => {
    disableDatabaseStorage();
    clearAllQuests();
  });
  afterEach(() => clearAllQuests());

  describe('quest chains', () => {
    it('exposes chain follow-ups for chainable base quest types', () => {
      const step = getQuestChainStep('talk_to_characters');
      expect(step).toBeDefined();
      expect(step!.reward).toBeGreaterThan(0);
      expect(getQuestChainStep('buy_item')).toBeUndefined();
    });

    it('unlocks a follow-up chain when the base quest completes', () => {
      const player = makePlayer();
      expect(isChainUnlocked(player.userId, 'earn_money')).toBe(false);

      const step = unlockQuestChain(player.userId, 'earn_money');
      expect(step).toBeDefined();
      expect(isChainUnlocked(player.userId, 'earn_money')).toBe(true);
    });

    it('does not progress a chain that has not been unlocked', () => {
      const player = makePlayer();
      const res = updateChainProgress(player.userId, 'earn_money', 4, player);
      expect(res).toBeNull();
    });

    it('progresses and completes an unlocked chain, awarding diamonds', () => {
      const player = makePlayer();
      unlockQuestChain(player.userId, 'earn_money');
      const step = getQuestChainStep('earn_money')!;

      const partial = updateChainProgress(player.userId, 'earn_money', 1, player);
      expect(partial!.completed).toBe(false);

      const done = updateChainProgress(player.userId, 'earn_money', step.required, player);
      expect(done!.completed).toBe(true);
      expect(done!.justCompleted).toBe(true);
      expect(player.c.diamonds).toBe(step.reward);

      // Idempotent: re-completing does not award again.
      const again = updateChainProgress(player.userId, 'earn_money', 5, player);
      expect(again!.justCompleted).toBe(false);
      expect(player.c.diamonds).toBe(step.reward);
    });
  });

  describe('all-quests streak bonus', () => {
    it('fires the bonus after N consecutive full-clear days', () => {
      const player = makePlayer();
      const days = ['2024-06-01', '2024-06-02', '2024-06-03'];

      let last = recordFullClear(player.userId, player, days[0]);
      expect(last.fired).toBe(false);
      expect(getFullClearStreak(player.userId)).toBe(1);

      last = recordFullClear(player.userId, player, days[1]);
      expect(last.fired).toBe(false);
      expect(getFullClearStreak(player.userId)).toBe(2);

      last = recordFullClear(player.userId, player, days[2]);
      expect(getFullClearStreak(player.userId)).toBe(STREAK_BONUS_THRESHOLD);
      expect(last.fired).toBe(true);
      expect(last.diamonds).toBe(STREAK_BONUS_REWARD);
      expect(player.c.diamonds).toBe(STREAK_BONUS_REWARD);
    });

    it('resets the streak when a day is skipped', () => {
      const player = makePlayer();
      recordFullClear(player.userId, player, '2024-06-01');
      recordFullClear(player.userId, player, '2024-06-02');
      // Skip 06-03; jump to 06-04 -> streak resets to 1.
      recordFullClear(player.userId, player, '2024-06-04');
      expect(getFullClearStreak(player.userId)).toBe(1);
    });

    it('is idempotent within the same day', () => {
      const player = makePlayer();
      recordFullClear(player.userId, player, '2024-06-01');
      const repeat = recordFullClear(player.userId, player, '2024-06-01');
      expect(repeat.fired).toBe(false);
      expect(getFullClearStreak(player.userId)).toBe(1);
    });
  });

  describe('weekly challenges', () => {
    it('rolls a deterministic weekly challenge per player+week', () => {
      const a1 = getOrRollWeeklyChallenge('px');
      const a2 = getOrRollWeeklyChallenge('px');
      expect(a1.id).toBe(a2.id);
      expect(getWeeklyChallengeTemplates().some(t => t.id === a1.id)).toBe(true);
    });

    it('grants the larger weekly payout on completion', () => {
      const player = makePlayer('weekly-player');
      const challenge = getOrRollWeeklyChallenge(player.userId);

      // Advance using the challenge's own quest type to its full requirement.
      const result = updateWeeklyChallengeProgress(
        player.userId,
        challenge.questType,
        challenge.progressRequired,
        player
      );
      expect(result).toBeDefined();
      expect(result!.completed).toBe(true);
      expect(result!.justCompleted).toBe(true);
      expect(player.c.diamonds).toBe(challenge.diamondReward);
      // Weekly payout is larger than the biggest daily reward (35).
      expect(challenge.diamondReward).toBeGreaterThan(35);
    });

    it('ignores progress that does not match the active challenge type', () => {
      const player = makePlayer('weekly-player-2');
      const challenge = getOrRollWeeklyChallenge(player.userId);
      const otherType = challenge.questType === 'earn_money' ? 'study' : 'earn_money';
      const result = updateWeeklyChallengeProgress(player.userId, otherType as never, 9999, player);
      expect(result).toBeNull();
    });
  });

  describe('persistence', () => {
    it('round-trips streak + weekly state through serialize/load', () => {
      const player = makePlayer('persist-player');

      recordFullClear(player.userId, player, '2024-06-01');
      recordFullClear(player.userId, player, '2024-06-02');
      const challenge = getOrRollWeeklyChallenge(player.userId);
      updateWeeklyChallengeProgress(player.userId, challenge.questType, 1, player);

      const snapshot = serializeQuestEngagement(player.userId);
      expect(snapshot.fullClearStreak).toBe(2);
      expect(snapshot.weekly).not.toBeNull();
      expect(snapshot.weekly!.progress).toBe(1);

      // Simulate a fresh process: clear, then hydrate from the snapshot.
      clearPlayerQuests(player.userId);
      expect(getFullClearStreak(player.userId)).toBe(0);

      loadQuestEngagement(player.userId, snapshot);
      expect(getFullClearStreak(player.userId)).toBe(2);
      const restored = getOrRollWeeklyChallenge(player.userId);
      expect(restored.id).toBe(challenge.id);
      expect(restored.progress).toBe(1);
    });
  });
});
