import { describe, it, expect, beforeEach } from 'vitest';
import {
  generateRoomType,
  getFloorArmor,
  getDodgeChance,
  getEnemyBarrier,
  generateFloorState,
  getPuzzleProgressSpeed,
} from '../utils/room-utils';
import { GUARDIANS, FLOOR_ELEM_CYCLE, PUZZLE_ROOMS, SWARM_CONFIG, SPEED_ROOM_CONFIG, FLOOR_ARMOR_CONFIG } from '../constants';
import type { EnemyState, FloorState } from '../types';
import { getFloorMaxHP, getFloorElement, getEnemyName } from '../utils/floor-utils';

// ─── generateRoomType ─────────────────────────────────────────────────────────

describe('generateRoomType', () => {
  // Use beforeEach to reset any mocked state before each test
  beforeEach(() => {
    // Restore Math.random after each test that might mock it
    Math.random = global.Math.random;
  });

  it('should return "guardian" for guardian floors', () => {
    // Get all guardian floors from GUARDIANS object
    for (const floor of Object.keys(GUARDIANS).map(Number)) {
      // Override and restore Math.random to ensure consistent result
      const originalRandom = Math.random;
      Math.random = () => 0.1; // Anything < PUZZLE_ROOM_CHANCE ensures non-puzzle
      expect(generateRoomType(floor)).toBe('guardian');
      Math.random = originalRandom;
    }
  });

  it('should return "guardian" for floor 10 (Ignis Prime)', () => {
    expect(generateRoomType(10)).toBe('guardian');
  });

  it('should return "guardian" for floor 50 (Lux Aeterna)', () => {
    expect(generateRoomType(50)).toBe('guardian');
  });

  it('should return "guardian" for floor 100 (The Awakened One)', () => {
    expect(generateRoomType(100)).toBe('guardian');
  });

  it('should return "puzzle" for floors divisible by PUZZLE_ROOM_INTERVAL with chance', () => {
    // Test floor 7 (first puzzle floor)
    // PUZZLE_ROOM_INTERVAL = 7, PUZZLE_ROOM_CHANCE = 0.20
    // Room type returns puzzle if:
    // 1. floor % 7 === 0 AND Math.random() < 0.20
    // 2. Math.random() < 0.15 (swarm chance)
    // 3. Math.random() < 0.10 (speed chance)
    // So if Math.random() < 0.20, it should be puzzle
    const originalRandom = Math.random;
    Math.random = () => 0.19; // < 0.20
    expect(generateRoomType(7)).toBe('puzzle');
    Math.random = originalRandom;
  });

  it('should return "puzzle" for floor 14', () => {
    const originalRandom = Math.random;
    Math.random = () => 0.19;
    expect(generateRoomType(14)).toBe('puzzle');
    Math.random = originalRandom;
  });

  it('should return "puzzle" for floor 21', () => {
    const originalRandom = Math.random;
    Math.random = () => 0.19;
    expect(generateRoomType(21)).toBe('puzzle');
    Math.random = originalRandom;
  });

  it('should return "puzzle" for floor 1000', () => {
    const originalRandom = Math.random;
    Math.random = () => 0.19;
    expect(generateRoomType(1000)).toBe('puzzle');
    Math.random = originalRandom;
  });

  it('should NOT return "puzzle" for floor 7 with low random', () => {
    const originalRandom = Math.random;
    Math.random = () => 0.25; // >= 0.20
    expect(generateRoomType(7)).not.toBe('puzzle');
    Math.random = originalRandom;
  });

  it('should return "swarm" for non-guardian floor with random < 0.15', () => {
    const originalRandom = Math.random;
    Math.random = () => 0.14; // < SWARM_ROOM_CHANCE (0.15)
    expect(generateRoomType(5)).toBe('swarm');
    Math.random = originalRandom;
  });

  it('should return "swarm" for floor 1 with swarm chance', () => {
    const originalRandom = Math.random;
    Math.random = () => 0.14;
    expect(generateRoomType(1)).toBe('swarm');
    Math.random = originalRandom;
  });

  it('should NOT return "swarm" for non-guardian floor with high random', () => {
    const originalRandom = Math.random;
    Math.random = () => 0.20; // >= 0.15
    expect(generateRoomType(5)).not.toBe('swarm');
    Math.random = originalRandom;
  });

  it('should return "speed" for non-guardian floor with random < 0.10', () => {
    const originalRandom = Math.random;
    Math.random = () => 0.09; // < SPEED_ROOM_CHANCE (0.10)
    expect(generateRoomType(5)).toBe('speed');
    Math.random = originalRandom;
  });

  it('should NOT return "speed" for non-guardian floor with high random', () => {
    const originalRandom = Math.random;
    Math.random = () => 0.11; // >= 0.10
    expect(generateRoomType(5)).not.toBe('speed');
    Math.random = originalRandom;
  });

  it('should return "combat" for non-guardian, non-special floors with high random', () => {
    const originalRandom = Math.random;
    Math.random = () => 0.50; // Won't match any special condition
    expect(generateRoomType(5)).toBe('combat');
    Math.random = originalRandom;
  });

  it('should return a valid RoomType', () => {
    for (let floor = 1; floor <= 100; floor++) {
      // Not mocking Math.random so we get varied results
      const roomType = generateRoomType(floor);
      expect(['combat', 'puzzle', 'swarm', 'speed', 'guardian']).toContain(roomType);
    }
  });
});

// ─── getFloorArmor ────────────────────────────────────────────────────────────

describe('getFloorArmor', () => {
  it('should return 0 for floors 1-9', () => {
    for (let floor = 1; floor <= 9; floor++) {
      expect(getFloorArmor(floor)).toBe(0);
    }
  });

  it('should calculate armor chance correctly starting at floor 10', () => {
    // At floor 10: armorChance = min(0.5, 0 + (10-10)*0.01) = 0
    // At floor 11: armorChance = 0.01
    // At floor 50: armorChance = min(0.5, 0 + 40*0.01) = 0.4
    const armorFor50 = getFloorArmor(50);
    expect(armorFor50).toBeGreaterThanOrEqual(0);
  });

  it('should return 0 when armor roll fails', () => {
    // Mock Math.random to simulate non-armor floor
    const originalRandom = Math.random;
    Math.random = () => 0.5; // > armor chance for all floors
    const armor = getFloorArmor(50);
    expect(armor).toBe(0);
    Math.random = originalRandom;
  });

  it('should return 0 for guardian floors', () => {
    expect(getFloorArmor(10)).toBe(0); // Ignis Prime has armor 0.10 in GUARDIANS
  });

  it('should return armor between min and max for non-guardian floor with armor', () => {
    // Mock Math.random to have armor succeed and return high value
    const originalRandom = Math.random;
    Math.random = () => 0;
    const armor = getFloorArmor(90);
    // Progress = min(1, (90-10)/90) = 80/90 = 0.888
    // Armor range = 0.25 - 0.05 = 0.20
    // Actual armor = 0.05 + 0.20 * 0.888 * Math.random()
    // With Math.random = 0, armor should be 0.05
    expect(armor).toBeGreaterThanOrEqual(FLOOR_ARMOR_CONFIG.minArmor);
    expect(armor).toBeLessThanOrEqual(FLOOR_ARMOR_CONFIG.maxArmor);
    Math.random = originalRandom;
  });

  it('should return armor between FLOOR_ARMOR_CONFIG.minArmor and maxArmor', () => {
    const originalRandom = Math.random;
    Math.random = () => 0.3; // Any value
    const armor = getFloorArmor(80);
    expect(armor).toBeGreaterThanOrEqual(FLOOR_ARMOR_CONFIG.minArmor);
    expect(armor).toBeLessThanOrEqual(FLOOR_ARMOR_CONFIG.maxArmor);
    Math.random = originalRandom;
  });

  it('should not exceed max armor for high floors', () => {
    const originalRandom = Math.random;
    Math.random = () => 0;
    const armor = getFloorArmor(1000);
    expect(armor).toBeLessThanOrEqual(FLOOR_ARMOR_CONFIG.maxArmor);
    Math.random = originalRandom;
  });

  it('should handle floor 0', () => {
    expect(getFloorArmor(0)).toBe(0);
  });
});

// ─── getDodgeChance ───────────────────────────────────────────────────────────

describe('getDodgeChance', () => {
  it('should increase with floor number', () => {
    const dodge1 = getDodgeChance(1);
    const dodge50 = getDodgeChance(50);
    const dodge100 = getDodgeChance(100);
    expect(dodge1).toBeLessThan(dodge50);
    expect(dodge50).toBeLessThan(dodge100);
  });

  it('should be SPEED_ROOM_CONFIG.baseDodgeChance at floor 1', () => {
    expect(getDodgeChance(1)).toBe(SPEED_ROOM_CONFIG.baseDodgeChance);
  });

  it('should be SPEED_ROOM_CONFIG.baseDodgeChance + 100*SPEED_ROOM_CONFIG.dodgePerFloor at floor 100', () => {
    // 0.25 + 100*0.005 = 0.75
    // But capped at maxDodge (0.50)
    expect(getDodgeChance(100)).toBe(SPEED_ROOM_CONFIG.maxDodge);
  });

  it('should never exceed SPEED_ROOM_CONFIG.maxDodge', () => {
    const originalRandom = Math.random;
    Math.random = () => 0.5;
    const dodge = getDodgeChance(1000);
    expect(dodge).toBeLessThanOrEqual(SPEED_ROOM_CONFIG.maxDodge);
    Math.random = originalRandom;
  });

  it('should be a number between 0 and 1', () => {
    for (let floor = 1; floor <= 100; floor++) {
      const dodge = getDodgeChance(floor);
      expect(dodge).toBeGreaterThanOrEqual(0);
      expect(dodge).toBeLessThanOrEqual(1);
    }
  });

  it('should handle floor 0', () => {
    expect(getDodgeChance(0)).toBe(0);
  });
});

// ─── getEnemyBarrier ──────────────────────────────────────────────────────────

describe('getEnemyBarrier', () => {
  it('should return 0 for floors below 20', () => {
    for (let floor = 1; floor < 20; floor++) {
      expect(getEnemyBarrier(floor, 'fire')).toBe(0);
    }
  });

  it('should return barrier proportionally for floor 20', () => {
    expect(getEnemyBarrier(20, 'fire')).toBeGreaterThan(0);
    // barrierChance = 0.08 + 0 (floor bonus) = 0.08
    // If Math.random() < 0.08 -> barrier exists
    // barrier = 0.1 + 0*floorProgress = 0.1
    // Floor progress for floor 20 = min(1, (20-20)/80) = 0
  });

  it('should return different barriers for different elements on same floor', () => {
    const fireBarrier = getEnemyBarrier(50, 'fire');
    const waterBarrier = getEnemyBarrier(50, 'water');
    const airBarrier = getEnemyBarrier(50, 'air');
    expect(typeof fireBarrier).toBe('number');
    expect(typeof waterBarrier).toBe('number');
    expect(typeof airBarrier).toBe('number');
  });

  it('should favor barrier elements more often', () => {
    // Barrier chance for fire: 0.08 + 0.09 (floor bonus) = 0.17
    // Barrier chance for light: 0.15 + 0.09 = 0.24
    const fireHasBarrier = getEnemyBarrier(50, 'fire') > 0;
    const lightHasBarrier = getEnemyBarrier(50, 'light') > 0;
    expect(typeof fireHasBarrier).toBe('boolean');
    expect(typeof lightHasBarrier).toBe('boolean');
  });

  it('barrier should be between 0.1 and 0.4 for floor 50', () => {
    const barrier = getEnemyBarrier(50, 'fire');
    expect(barrier).toBeGreaterThanOrEqual(0.1);
    expect(barrier).toBeLessThanOrEqual(0.4);
  });

  it('should return 0 when barrier roll fails', () => {
    const originalRandom = Math.random;
    Math.random = () => 0.5; // > barrier chance
    const barrier = getEnemyBarrier(50, 'fire');
    expect(barrier).toBe(0);
    Math.random = originalRandom;
  });

  it('should cap at 0.4 maximum barrier', () => {
    const originalRandom = Math.random;
    Math.random = () => 0; // Always succeeds
    const barrier = getEnemyBarrier(200, 'fire');
    expect(barrier).toBeLessThanOrEqual(0.4);
    Math.random = originalRandom;
  });

  it('should handle all elements', () => {
    const elements = ['fire', 'water', 'air', 'earth', 'light', 'dark', 'death'];
    for (const elem of elements) {
      const barrier = getEnemyBarrier(50, elem);
      expect(typeof barrier).toBe('number');
    }
  });
});

// ─── generateFloorState ───────────────────────────────────────────────────────

describe('generateFloorState', () => {
  it('should return a FloorState object', () => {
    const state = generateFloorState(1);
    expect(typeof state).toBe('object');
    expect(state).toHaveProperty('roomType');
    expect(state).toHaveProperty('enemies');
  });

  it('should generate guardian state for guardian floor', () => {
    const state = generateFloorState(10);
    expect(state.roomType).toBe('guardian');
    expect(state.enemies.length).toBe(1);
    expect(state.enemies[0].name).toBe(GUARDIANS[10].name);
    expect(state.enemies[0].hp).toBe(GUARDIANS[10].hp);
    expect(state.enemies[0].element).toBe(GUARDIANS[10].element);
  });

  it('should generate combat state for non-guardian floor with combat', () => {
    const originalRandom = Math.random;
    Math.random = () => 0.5; // Won't trigger special rooms
    const state = generateFloorState(5);
    expect(state.roomType).toBe('combat');
    expect(state.enemies.length).toBe(1);
    expect(state.enemies[0].hp).toBe(getFloorMaxHP(5));
    Math.random = originalRandom;
  });

  it('should generate swarm state for swarm room', () => {
    const originalRandom = Math.random;
    Math.random = () => 0.14; // < SWARM_ROOM_CHANCE (0.15)
    const state = generateFloorState(5);
    expect(state.roomType).toBe('swarm');
    expect(Array.isArray(state.enemies)).toBe(true);
    expect(state.enemies.length).toBeGreaterThanOrEqual(SWARM_CONFIG.minEnemies);
    Math.random = originalRandom;
  });

  it('should generate speed state for speed room', () => {
    const originalRandom = Math.random;
    Math.random = () => 0.09; // < SPEED_ROOM_CHANCE (0.10)
    const state = generateFloorState(5);
    expect(state.roomType).toBe('speed');
    expect(state.enemies.length).toBe(1);
    Math.random = originalRandom;
  });

  it('should generate puzzle state for puzzle room', () => {
    const originalRandom = Math.random;
    Math.random = () => 0.19; // < PUZZLE_ROOM_CHANCE (0.20)
    const state = generateFloorState(7);
    expect(state.roomType).toBe('puzzle');
    expect(Array.isArray(state.enemies)).toBe(true); // Array, even if empty
    expect(state.enemies).toEqual([]);
    expect(state.puzzleProgress).toBe(0); // Empty array is truthy, empty is falsy
    expect(typeof state.puzzleRequired).toBe('number');
    expect(typeof state.puzzleId).toBe('string');
    expect(typeof state.puzzleAttunements).toBe('object');
    Math.random = originalRandom;
  });

  it('should fill puzzle attunements from PUZZLE_ROOMS', () => {
    const originalRandom = Math.random;
    Math.random = () => 0.19;
    const state = generateFloorState(7);
    expect(state.roomType).toBe('puzzle');
    expect(state.puzzleAttunements.length).toBeGreaterThan(0);
    expect(typeof state.puzzleAttunements[0]).toBe('string');
    Math.random = originalRandom;
  });

  it('should use correct element for floor', () => {
    // Test multiple floors to verify element cycle
    const state = generateFloorState(1);
    expect(state.enemies[0].element).toBe('fire');

    const state2 = generateFloorState(2);
    expect(state2.enemies[0].element).toBe('water');
  });

  it('combat enemy HP should match floor max HP', () => {
    const state = generateFloorState(50);
    // Non-guardian floor 50 returns combat
    expect(state.enemies[0].hp).toBe(getFloorMaxHP(50));
  });

  it('speed room should have correct dodge chance', () => {
    const state = generateFloorState(50);
    const originalRandom = Math.random;
    Math.random = () => 0.09; // Speed room
    const speedState = generateFloorState(50);
    expect(speedState.roomType).toBe('speed');
    expect(speedState.enemies[0].dodgeChance).toBe(getDodgeChance(50));
    Math.random = originalRandom;
  });

  it('should handle very high floor number', () => {
    const state = generateFloorState(1000);
    expect(state.roomType).toBe('guardian');
  });

  it('should handle floor 0', () => {
    const state = generateFloorState(0);
    expect(state.roomType).toBe('combat'); // Guardian? No. Special room? No. Default combat.
  });
});

// ─── getPuzzleProgressSpeed ───────────────────────────────────────────────────

describe('getPuzzleProgressSpeed', () => {
  it('should return a positive number', () => {
    const st: Record<string, any> = {};
    const speed = getPuzzleProgressSpeed('enchanter_trial', st);
    expect(typeof speed).toBe('number');
    expect(speed).toBeGreaterThan(0);
  });

  it('should return baseProgressPerTick for without attunements', () => {
    const st: Record<string, any> = {};
    const puzzle = PUZZLE_ROOMS.enchanter_trial;
    const speed = getPuzzleProgressSpeed('enchanter_trial', st);
    expect(speed).toBe(puzzle.baseProgressPerTick);
  });

  it('should return baseProgressPerTick + bonus for with attunement', () => {
    const state = {
      enchanter: { active: true, level: 1 }
    };
    const puzzle = PUZZLE_ROOMS.enchanter_trial;
    const speed = getPuzzleProgressSpeed('enchanter_trial', state);
    const expected = puzzle.baseProgressPerTick + puzzle.attunementBonus * 1;
    expect(speed).toBe(expected);
  });

  it('should return baseProgressPerTick + bonuses for with multiple attunements', () => {
    const state = {
      enchanter: { active: true, level: 2 },
      fabricator: { active: true, level: 3 }
    };
    const speed = getPuzzleProgressSpeed('fabricator_trial', state);
    const base = PUZZLE_ROOMS.fabricator_trial.baseProgressPerTick;
    const bonus = PUZZLE_ROOMS.fabricator_trial.attunementBonus * 3;
    expect(speed).toBe(base + bonus);
  });

  it('should return baseProgressPerTick + bonuses for hybrid puzzle with two attunements', () => {
    const state = {
      enchanter: { active: true, level: 1 },
      fabricator: { active: true, level: 1 }
    };
    const speed = getPuzzleProgressSpeed('hybrid_enchanter_fabricator', state);
    const base = PUZZLE_ROOMS.hybrid_enchanter_fabricator.baseProgressPerTick;
    const bonus = PUZZLE_ROOMS.hybrid_enchanter_fabricator.attunementBonus * 2;
    expect(speed).toBe(base + bonus);
  });

  it('should handle empty attunement state', () => {
    const speed = getPuzzleProgressSpeed('invoker_trial', {});
    expect(speed).toBe(PUZZLE_ROOMS.invoker_trial.baseProgressPerTick);
  });

  it('should handle non-existent puzzle', () => {
    const speed = getPuzzleProgressSpeed('nonexistent_puzzle', {});
    expect(speed).toBe(0.02); // Default fallback
  });

  it('should return different speeds for different puzzle types', () => {
    const state = { enchanter: { active: true, level: 1 } };
    const speed1 = getPuzzleProgressSpeed('enchanter_trial', state);
    const speed2 = getPuzzleProgressSpeed('fabricator_trial', state);
    expect(speed1).not.toBe(speed2);
  });

  it('should handle null level', () => {
    const state = {
      enchanter: { active: true, level: null }
    };
    const speed = getPuzzleProgressSpeed('enchanter_trial', state);
    const expected = PUZZLE_ROOMS.enchanter_trial.baseProgressPerTick +
                     PUZZLE_ROOMS.enchanter_trial.attunementBonus * 1;
    expect(speed).toBe(expected);
  });

  it('should handle level 0', () => {
    const state = {
      enchanter: { active: true, level: 0 }
    };
    const speed = getPuzzleProgressSpeed('enchanter_trial', state);
    const expected = PUZZLE_ROOMS.enchanter_trial.baseProgressPerTick +
                     PUZZLE_ROOMS.enchanter_trial.attunementBonus * 1;
    expect(speed).toBe(expected);
  });
});