/**
 * Daily Quests Service Tests
 * Tests the daily quests business logic
 */
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import {
  generateDailyQuests,
  updateQuestProgress,
  getActiveQuests,
  claimQuestReward,
  getQuestStatistics,
  formatQuestForClient,
  getQuestTemplate,
  getAllQuestTemplates,
  clearPlayerQuests,
  clearAllQuests,
  enableDatabaseStorage,
  disableDatabaseStorage,
  isDatabaseStorageEnabled,
  type QuestType,
  type ActiveQuest,
} from '../../../src/services/retention/dailyQuests.js';
import { Person } from '../../../src/models/Person.js';
import { Player } from '../../../src/models/Player.js';
import { handlePerformActivity } from '../../../src/handlers/performActivity.js';

describe('Daily Quests Service', () => {
  beforeEach(() => {
    // Use in-memory storage for tests
    disableDatabaseStorage();
    clearAllQuests();
  });

  afterEach(() => {
    clearAllQuests();
  });

  describe('getAllQuestTemplates', () => {
    it('should return list of quest templates', () => {
      const templates = getAllQuestTemplates();

      expect(Array.isArray(templates)).toBe(true);
      expect(templates.length).toBe(9); // 3 easy + 3 medium + 3 hard (attend_class + work_hours pruned: no completion path)
    });

    it('should have templates with required properties', () => {
      const templates = getAllQuestTemplates();

      templates.forEach(template => {
        expect(template).toHaveProperty('type');
        expect(template).toHaveProperty('desc');
        expect(template).toHaveProperty('required');
        expect(template).toHaveProperty('reward');
        expect(template).toHaveProperty('difficulty');
        expect(template).toHaveProperty('energy');
        expect(template).toHaveProperty('icon');
      });
    });

    it('should have templates organized by difficulty', () => {
      const templates = getAllQuestTemplates();

      const easy = templates.filter(t => t.difficulty === 'easy');
      const medium = templates.filter(t => t.difficulty === 'medium');
      const hard = templates.filter(t => t.difficulty === 'hard');

      expect(easy.length).toBe(3);
      expect(medium.length).toBe(3);
      expect(hard.length).toBe(3);
    });

    it('should have templates with positive rewards', () => {
      const templates = getAllQuestTemplates();

      templates.forEach(template => {
        expect(template.reward).toBeGreaterThan(0);
      });
    });
  });

  describe('getQuestTemplate', () => {
    it('should return template by type', () => {
      const template = getQuestTemplate('talk_to_characters');

      expect(template).toBeDefined();
      expect(template?.type).toBe('talk_to_characters');
      expect(template?.difficulty).toBe('easy');
    });

    it('should return undefined for unknown type', () => {
      const template = getQuestTemplate('unknown_type' as QuestType);

      expect(template).toBeUndefined();
    });
  });

  describe('generateDailyQuests', () => {
    it('should generate 3 quests for new player', async () => {
      const playerId = 'test-player-' + Date.now();
      const quests = await generateDailyQuests(playerId);

      expect(quests).toHaveLength(3);
    });

    it('should generate one quest of each difficulty', async () => {
      const playerId = 'test-player-' + Date.now();
      const quests = await generateDailyQuests(playerId);

      const difficulties = quests.map(q => q.difficulty);
      expect(difficulties).toContain('easy');
      expect(difficulties).toContain('medium');
      expect(difficulties).toContain('hard');
    });

    it('should return existing quests if already generated today', async () => {
      const playerId = 'test-player-' + Date.now();
      const quests1 = await generateDailyQuests(playerId);
      const quests2 = await generateDailyQuests(playerId);

      expect(quests1.length).toBe(3);
      expect(quests2.length).toBe(3);
      // Should be the same quests
      expect(quests1[0].id).toBe(quests2[0].id);
    });

    it('should initialize quests with zero progress', async () => {
      const playerId = 'test-player-' + Date.now();
      const quests = await generateDailyQuests(playerId);

      quests.forEach(quest => {
        expect(quest.progress).toBe(0);
        expect(quest.completed).toBe(false);
      });
    });

    it('should have quests with required properties', async () => {
      const playerId = 'test-player-' + Date.now();
      const quests = await generateDailyQuests(playerId);

      quests.forEach(quest => {
        expect(quest).toHaveProperty('id');
        expect(quest).toHaveProperty('questType');
        expect(quest).toHaveProperty('description');
        expect(quest).toHaveProperty('progress');
        expect(quest).toHaveProperty('progressRequired');
        expect(quest).toHaveProperty('diamondReward');
        expect(quest).toHaveProperty('difficulty');
        expect(quest).toHaveProperty('iconName');
        expect(quest).toHaveProperty('completed');
      });
    });

    it('should fallback to in-memory generation if database mode fails', async () => {
      const playerId = 'db-fallback-' + Date.now();

      enableDatabaseStorage();
      expect(isDatabaseStorageEnabled()).toBe(true);

      const quests = await generateDailyQuests(playerId);

      expect(quests).toHaveLength(3);
      expect(isDatabaseStorageEnabled()).toBe(false);
    });
  });

  describe('getActiveQuests', () => {
    it('should return empty array for player with no quests', async () => {
      const playerId = 'no-quests-player-' + Date.now();
      const quests = await getActiveQuests(playerId);

      expect(quests).toEqual([]);
    });

    it('should return generated quests', async () => {
      const playerId = 'test-player-' + Date.now();
      await generateDailyQuests(playerId);
      const quests = await getActiveQuests(playerId);

      expect(quests).toHaveLength(3);
    });

    it('should sort quests by difficulty (hard first)', async () => {
      const playerId = 'test-player-' + Date.now();
      await generateDailyQuests(playerId);
      const quests = await getActiveQuests(playerId);

      expect(quests[0].difficulty).toBe('hard');
      expect(quests[1].difficulty).toBe('medium');
      expect(quests[2].difficulty).toBe('easy');
    });
  });

  describe('updateQuestProgress', () => {
    it('should update quest progress', async () => {
      const playerId = 'progress-test-' + Date.now();
      await generateDailyQuests(playerId);

      // Get the easy quest type
      const quests = await getActiveQuests(playerId);
      const easyQuest = quests.find(q => q.difficulty === 'easy');
      expect(easyQuest).toBeDefined();

      const result = await updateQuestProgress(playerId, easyQuest!.questType, 1);

      expect(result).not.toBeNull();
      const expectedProgress = Math.min(1, easyQuest!.progressRequired);
      expect(result!.progress).toBe(expectedProgress);
      expect(result!.completed).toBe(easyQuest!.progressRequired <= 1);
    });

    it('should cap progress at required amount', async () => {
      const playerId = 'cap-test-' + Date.now();
      await generateDailyQuests(playerId);

      const quests = await getActiveQuests(playerId);
      const easyQuest = quests.find(q => q.difficulty === 'easy');
      expect(easyQuest).toBeDefined();

      // Update with more than required
      const result = await updateQuestProgress(playerId, easyQuest!.questType, 100);

      expect(result).not.toBeNull();
      expect(result!.progress).toBe(easyQuest!.progressRequired);
    });

    it('should mark quest as completed when target reached', async () => {
      const playerId = 'complete-test-' + Date.now();
      await generateDailyQuests(playerId);

      const quests = await getActiveQuests(playerId);
      const easyQuest = quests.find(q => q.difficulty === 'easy');
      expect(easyQuest).toBeDefined();

      // Complete the quest
      const result = await updateQuestProgress(playerId, easyQuest!.questType, easyQuest!.progressRequired);

      expect(result).not.toBeNull();
      expect(result!.completed).toBe(true);
      expect(result!.justCompleted).toBe(true);
    });

    it('should return null for quest type not assigned today', async () => {
      const playerId = 'wrong-type-test-' + Date.now();
      await generateDailyQuests(playerId);

      // Try to update a quest type that may not be assigned
      // This should return null if the type isn't in today's quests
      const quests = await getActiveQuests(playerId);
      const assignedTypes = quests.map(q => q.questType);

      // Find a type that isn't assigned
      const allTypes: QuestType[] = ['talk_to_characters', 'buy_item', 'attend_class', 'socialize',
        'work_hours', 'go_on_date', 'complete_activities', 'study',
        'spend_energy', 'earn_money', 'increase_affinity'];
      const unassignedType = allTypes.find(t => !assignedTypes.includes(t));

      if (unassignedType) {
        const result = await updateQuestProgress(playerId, unassignedType, 1);
        expect(result).toBeNull();
      }
    });

    it('should return null for player with no quests', async () => {
      const playerId = 'no-quests-' + Date.now();
      const result = await updateQuestProgress(playerId, 'talk_to_characters', 1);

      expect(result).toBeNull();
    });
  });

  describe('claimQuestReward', () => {
    it('should claim reward for completed quest', async () => {
      const playerId = 'claim-test-' + Date.now();
      await generateDailyQuests(playerId);

      const quests = await getActiveQuests(playerId);
      const easyQuest = quests.find(q => q.difficulty === 'easy');
      expect(easyQuest).toBeDefined();

      // Complete the quest
      await updateQuestProgress(playerId, easyQuest!.questType, easyQuest!.progressRequired);

      // Claim the reward
      const result = await claimQuestReward(playerId, easyQuest!.id);

      expect(result.success).toBe(true);
      expect(result.reward).not.toBeNull();
      expect(result.reward!.diamonds).toBe(easyQuest!.diamondReward);
    });

    it('should fail if quest not completed', async () => {
      const playerId = 'incomplete-test-' + Date.now();
      await generateDailyQuests(playerId);

      const quests = await getActiveQuests(playerId);
      const quest = quests[0];

      const result = await claimQuestReward(playerId, quest.id);

      expect(result.success).toBe(false);
      expect(result.message).toBe('Quest not completed yet');
    });

    it('should fail for already claimed quest', async () => {
      const playerId = 'already-claimed-' + Date.now();
      await generateDailyQuests(playerId);

      const quests = await getActiveQuests(playerId);
      const easyQuest = quests.find(q => q.difficulty === 'easy');
      expect(easyQuest).toBeDefined();

      // Complete and claim
      await updateQuestProgress(playerId, easyQuest!.questType, easyQuest!.progressRequired);
      await claimQuestReward(playerId, easyQuest!.id);

      // Try to claim again
      const result = await claimQuestReward(playerId, easyQuest!.id);

      expect(result.success).toBe(false);
      expect(result.message).toBe('Reward already claimed');
    });

    it('should fail for non-existent quest', async () => {
      const playerId = 'non-existent-' + Date.now();
      await generateDailyQuests(playerId);

      const result = await claimQuestReward(playerId, 'non-existent-quest-id');

      expect(result.success).toBe(false);
      expect(result.message).toBe('Quest not found');
    });

    it('should fail for player with no quests', async () => {
      const playerId = 'no-quests-claim-' + Date.now();
      const result = await claimQuestReward(playerId, 'some-quest-id');

      expect(result.success).toBe(false);
    });
  });

  describe('getQuestStatistics', () => {
    it('should return empty stats for player with no quests', async () => {
      const playerId = 'no-stats-' + Date.now();
      const stats = await getQuestStatistics(playerId);

      expect(stats.totalCompleted).toBe(0);
      expect(stats.totalDiamondsEarned).toBe(0);
      expect(stats.daysWithQuests).toBe(0);
      expect(stats.byDifficulty).toEqual({});
    });

    it('should track completed quests', async () => {
      const playerId = 'stats-test-' + Date.now();
      await generateDailyQuests(playerId);

      const quests = await getActiveQuests(playerId);
      const easyQuest = quests.find(q => q.difficulty === 'easy');
      expect(easyQuest).toBeDefined();

      // Complete the quest
      await updateQuestProgress(playerId, easyQuest!.questType, easyQuest!.progressRequired);

      const stats = await getQuestStatistics(playerId);

      expect(stats.totalCompleted).toBe(1);
      expect(stats.totalDiamondsEarned).toBe(easyQuest!.diamondReward);
      expect(stats.byDifficulty['easy']).toBe(1);
    });
  });

  describe('formatQuestForClient', () => {
    it('should format quest for iOS client', async () => {
      const playerId = 'format-test-' + Date.now();
      await generateDailyQuests(playerId);

      const quests = await getActiveQuests(playerId);
      const quest = quests[0];
      const formatted = formatQuestForClient(quest);

      expect(formatted).toHaveProperty('id');
      expect(formatted).toHaveProperty('name');
      expect(formatted).toHaveProperty('description');
      expect(formatted).toHaveProperty('category');
      expect(formatted).toHaveProperty('reward');
      expect(formatted).toHaveProperty('progress');
      expect(formatted).toHaveProperty('target');
      expect(formatted).toHaveProperty('completed');
      expect(formatted).toHaveProperty('claimed');
    });

    it('should capitalize category', async () => {
      const playerId = 'category-test-' + Date.now();
      await generateDailyQuests(playerId);

      const quests = await getActiveQuests(playerId);
      const formatted = formatQuestForClient(quests[0]);

      // Category should be capitalized
      expect(formatted.category[0]).toBe(formatted.category[0].toUpperCase());
    });

    it('should include reward object with diamonds', async () => {
      const playerId = 'reward-format-' + Date.now();
      await generateDailyQuests(playerId);

      const quests = await getActiveQuests(playerId);
      const formatted = formatQuestForClient(quests[0]);

      expect(formatted.reward).toHaveProperty('diamonds');
      expect(formatted.reward).toHaveProperty('energy');
      expect(formatted.reward).toHaveProperty('money');
      expect(formatted.reward.diamonds).toBeGreaterThan(0);
    });

    it('should map quest types to appropriate categories', () => {
      const questTypeCategories: Record<QuestType, string> = {
        talk_to_characters: 'Social',
        socialize: 'Social',
        go_on_date: 'Social',
        increase_affinity: 'Social',
        work_hours: 'Career',
        earn_money: 'Wealth',
        buy_item: 'Wealth',
        attend_class: 'Education',
        study: 'Education',
        complete_activities: 'Activities',
        spend_energy: 'Activities',
      };

      for (const [questType, expectedCategory] of Object.entries(questTypeCategories)) {
        const mockQuest: ActiveQuest = {
          id: 'test-id',
          questType: questType as QuestType,
          description: 'Test description',
          progress: 0,
          progressRequired: 10,
          diamondReward: 10,
          difficulty: 'easy',
          iconName: 'test',
          completed: false,
        };

        const formatted = formatQuestForClient(mockQuest);
        expect(formatted.category).toBe(expectedCategory);
      }
    });

    it('should keep completed quests claimable until reward is claimed', async () => {
      const playerId = 'claimable-format-' + Date.now();
      await generateDailyQuests(playerId);

      const quests = await getActiveQuests(playerId);
      const easyQuest = quests.find(q => q.difficulty === 'easy');
      expect(easyQuest).toBeDefined();

      await updateQuestProgress(playerId, easyQuest!.questType, easyQuest!.progressRequired);

      const refreshed = await getActiveQuests(playerId);
      const completedQuest = refreshed.find(q => q.id === easyQuest!.id);
      expect(completedQuest).toBeDefined();
      expect(completedQuest!.completed).toBe(true);

      const formatted = formatQuestForClient(completedQuest!);
      expect(formatted.completed).toBe(true);
      expect(formatted.claimed).toBe(false);
    });

    it('should mark quest as claimed after reward claim', async () => {
      const playerId = 'claimed-format-' + Date.now();
      await generateDailyQuests(playerId);

      const quests = await getActiveQuests(playerId);
      const easyQuest = quests.find(q => q.difficulty === 'easy');
      expect(easyQuest).toBeDefined();

      await updateQuestProgress(playerId, easyQuest!.questType, easyQuest!.progressRequired);
      const claimResult = await claimQuestReward(playerId, easyQuest!.id);
      expect(claimResult.success).toBe(true);

      const refreshed = await getActiveQuests(playerId);
      const claimedQuest = refreshed.find(q => q.id === easyQuest!.id);
      expect(claimedQuest).toBeDefined();

      const formatted = formatQuestForClient(claimedQuest!);
      expect(formatted.completed).toBe(true);
      expect(formatted.claimed).toBe(true);
    });
  });

  describe('clearPlayerQuests', () => {
    it('should clear quests for specific player', async () => {
      const playerId = 'clear-test-' + Date.now();
      await generateDailyQuests(playerId);

      clearPlayerQuests(playerId);

      const quests = await getActiveQuests(playerId);
      expect(quests).toEqual([]);
    });

    it('should not affect other players', async () => {
      const playerId1 = 'player1-' + Date.now();
      const playerId2 = 'player2-' + Date.now();

      await generateDailyQuests(playerId1);
      await generateDailyQuests(playerId2);

      clearPlayerQuests(playerId1);

      const quests1 = await getActiveQuests(playerId1);
      const quests2 = await getActiveQuests(playerId2);

      expect(quests1).toEqual([]);
      expect(quests2).toHaveLength(3);
    });
  });

  describe('Quest difficulty rewards', () => {
    it('should have increasing rewards by difficulty', () => {
      const templates = getAllQuestTemplates();

      const easyRewards = templates.filter(t => t.difficulty === 'easy').map(t => t.reward);
      const mediumRewards = templates.filter(t => t.difficulty === 'medium').map(t => t.reward);
      const hardRewards = templates.filter(t => t.difficulty === 'hard').map(t => t.reward);

      const avgEasy = easyRewards.reduce((a, b) => a + b, 0) / easyRewards.length;
      const avgMedium = mediumRewards.reduce((a, b) => a + b, 0) / mediumRewards.length;
      const avgHard = hardRewards.reduce((a, b) => a + b, 0) / hardRewards.length;

      expect(avgMedium).toBeGreaterThan(avgEasy);
      expect(avgHard).toBeGreaterThan(avgMedium);
    });
  });
});

// ===========================================================================
// T003 proof: newly-wired daily-quest progress fires via handlePerformActivity
// EXACTLY ONCE per action (no loop tick also advances it, no double-count).
// ===========================================================================

class QuestProofSession {
  player: Player;
  sentMessages: any[] = [];
  saveCount = 0;
  constructor(player: Player) {
    this.player = player;
  }
  send(message: any) {
    this.sentMessages.push(message);
  }
  sendPlayerObject() {
    this.sentMessages.push({ type: 'playerObject' });
  }
  async savePlayer() {
    this.saveCount += 1;
  }
  get isWSOpen() {
    return true;
  }
}

function makeQuestProofSession(): QuestProofSession {
  const character = new Person({
    id: 'char-1',
    firstname: 'Test',
    lastname: 'Player',
    sex: 'Male',
    ageYears: 25,
    money: 1000,
    energy: 100,
    health: 90,
    happiness: 70,
    social: 50,
    intelligence: 50,
    creativity: 50,
    stress: 10,
    affinity: 50,
    // Several free evening slots so multiple immediate activities can run.
    dailyPlan: [
      { time: 18, location: 'home', title: 'Relax', name: 'home' },
      { time: 19, location: 'home', title: 'Relax', name: 'relax' },
      { time: 20, location: 'home', title: 'Relax', name: 'leisure' },
      { time: 21, location: 'home', title: 'Relax', name: 'play' },
    ] as any,
  });
  const player = new Player({
    userId: 'quest-proof-user',
    character,
    r: [],
    status: 'playing',
    date: '2024-06-15',
    hourOfDay: 10,
    minuteOfHour: 0,
  });
  return new QuestProofSession(player);
}

describe('T003 wired quest progress via handlePerformActivity (single-fire)', () => {
  beforeEach(() => {
    disableDatabaseStorage();
    clearAllQuests();
  });
  afterEach(() => {
    clearAllQuests();
    vi.restoreAllMocks();
  });

  it('advances complete_activities by exactly N when the activity fires N times, and is claimable once', async () => {
    const session = makeQuestProofSession();
    const playerId = session.player.userId;

    // Force generation to select complete_activities (medium index 2 of 4).
    // 0.6 -> floor(0.6*len): easy(4)->2, medium(4)->2 (complete_activities), hard(3)->1.
    vi.spyOn(Math, 'random').mockReturnValue(0.6);
    await generateDailyQuests(playerId);
    vi.spyOn(Math, 'random').mockRestore();

    const before = (await getActiveQuests(playerId)).find(q => q.questType === 'complete_activities');
    expect(before).toBeDefined();
    expect(before!.progress).toBe(0);

    // Fire 3 immediate activities (hobby is cheap, no minAge). Use a fixed RNG for
    // the activity outcome roll so it stays deterministic.
    for (let i = 0; i < 3; i++) {
      await handlePerformActivity({ activityId: 'hobby' }, session as any, () => 0.5);
    }

    const after = (await getActiveQuests(playerId)).find(q => q.questType === 'complete_activities');
    // Exactly +1 per action -> N=3. No loop tick also advanced it (there is no
    // loop-side hook for complete_activities; onActivityCompleted is never called).
    expect(after!.progress).toBe(3);

    // The performed-activity message was sent once per action.
    expect(session.sentMessages.filter(m => m?.type === 'activityPerformed').length).toBe(3);
  });

  it('advances spend_energy by the energy actually spent, once per action (no double-count)', async () => {
    const session = makeQuestProofSession();
    const playerId = session.player.userId;

    // 0.9 -> hard(3)->floor(2.7)=2 (increase_affinity)... pick spend_energy via 0.0 (hard index 0).
    vi.spyOn(Math, 'random').mockReturnValue(0.0);
    await generateDailyQuests(playerId);
    vi.spyOn(Math, 'random').mockRestore();

    const spendEnergyQuest = (await getActiveQuests(playerId)).find(q => q.questType === 'spend_energy');
    expect(spendEnergyQuest).toBeDefined();

    // hobby costs 6 energy. Fire twice -> spend_energy should advance by exactly 12.
    await handlePerformActivity({ activityId: 'hobby' }, session as any, () => 0.5);
    await handlePerformActivity({ activityId: 'hobby' }, session as any, () => 0.5);

    const after = (await getActiveQuests(playerId)).find(q => q.questType === 'spend_energy');
    expect(after!.progress).toBe(12);
  });
});
