import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { applyAction, decideEnemyAction, stepUntilPlayerTurn } from '../simulation/simulation';
import { type World, type Actor, type EntityId, type CombatantActor } from '../../core/types';
import { EntityManager } from '../EntityManager';

const createTestWorld = (actors: Map<EntityId, Actor>): World => {
    return {
        width: 10,
        height: 10,
        tiles: new Array(100).fill(0),
        actors,
        exit: { x: 9, y: 9 }
    };
  };
  
const createTestStats = (overrides: Partial<any> = {}) => ({
    maxHp: 20, hp: 20, attack: 5, defense: 2, level: 1, exp: 0, expToNextLevel: 10,
    statPoints: 0, skillPoints: 0, strength: 10, dexterity: 10, intelligence: 10, passiveNodes: [],
    critChance: 0, critMultiplier: 100, accuracy: 100, lifesteal: 0, evasion: 0, blockChance: 0, luck: 0,
    ...overrides
});

describe('Combat Simulation', () => {
  let entityManager: EntityManager;



  describe('applyAction', () => {
    it('should return empty events if actor does not exist', () => {
        const world = createTestWorld(new Map());
        const events = applyAction(world, 999, { type: "wait" });
        expect(events).toEqual([]);
    });
  });

  describe('applyAction - success paths', () => {
    it('should deal damage when player attacks enemy', () => {
      const actors = new Map<EntityId, Actor>();
      actors.set(1, {
        id: 1, category: "combatant", isPlayer: true, type: "player", pos: { x: 3, y: 3 }, speed: 100, stats: createTestStats(), energy: 0
      } as any);
      actors.set(2, {
        id: 2, category: "combatant", isPlayer: false, type: "rat", pos: { x: 4, y: 3 }, speed: 100, stats: createTestStats({ maxHp: 10, hp: 10, attack: 3, defense: 1 }), energy: 0
      } as any);

      const world = createTestWorld(actors);
      entityManager = new EntityManager(world);
      const events = applyAction(world, 1, { type: "attack", targetId: 2 }, entityManager);

      const enemy = world.actors.get(2) as CombatantActor;
      expect(enemy.stats.hp).toBeLessThan(10);
      expect(events.some(e => e.type === "attacked")).toBe(true);
    });

    it("should kill enemy and spawn EXP orb without ID reuse collision", () => {
      const actors = new Map<EntityId, Actor>();
      actors.set(1, {
        id: 1, category: "combatant", isPlayer: true, type: "player", pos: { x: 3, y: 3 }, speed: 100, stats: createTestStats({ attack: 50 }), energy: 0
      } as any);
      actors.set(2, {
        id: 2, category: "combatant", isPlayer: false, type: "rat", pos: { x: 4, y: 3 }, speed: 100, stats: createTestStats({ maxHp: 10, hp: 10, attack: 3, defense: 1 }), energy: 0
      } as any);

      const world = createTestWorld(actors);
      entityManager = new EntityManager(world);
      applyAction(world, 1, { type: "attack", targetId: 2 }, entityManager);

      // Enemy (id 2) should be gone
      expect(world.actors.has(2)).toBe(false);
      
      // A new ID should be generated for the orb (should be 3)
      const orb = [...world.actors.values()].find(a => a.type === "exp_orb");
      expect(orb).toBeDefined();
      expect(orb!.id).toBe(3);
    });

    it("should destruction tile when walking on destructible-by-walk tile", () => {
      const actors = new Map<EntityId, Actor>();
      actors.set(1, {
        id: 1, category: "combatant", isPlayer: true, type: "player", pos: { x: 3, y: 3 }, speed: 100, stats: createTestStats(), energy: 0
      } as any);

      const world = createTestWorld(actors);
      // tile at 4,3 is grass (15) which is destructible by walk
      const grassIdx = 3 * 10 + 4;
      world.tiles[grassIdx] = 15; // TileType.GRASS

      entityManager = new EntityManager(world);
      applyAction(world, 1, { type: "move", dx: 1, dy: 0 }, entityManager);

      // Player moved to 4,3
      const player = world.actors.get(1);
      expect(player!.pos).toEqual({ x: 4, y: 3 });

      // Tile should effectively be destroyed (turned to saplings/2)
      expect(world.tiles[grassIdx]).toBe(2); // TileType.GRASS_SAPLINGS
    });

    it("should handle wait action", () => {
        const actors = new Map<EntityId, Actor>();
        actors.set(1, { id: 1, category: "combatant", isPlayer: true, pos: { x: 0, y: 0 } } as any);
        const world = createTestWorld(actors);

        const events = applyAction(world, 1, { type: "wait" }, new EntityManager(world));
        expect(events).toEqual([{ type: "waited", actorId: 1 }]);
    });

    it("should default to wait for unknown action type", () => {
        const actors = new Map<EntityId, Actor>();
        actors.set(1, { id: 1, category: "combatant", isPlayer: true, pos: { x: 0, y: 0 } } as any);
        const world = createTestWorld(actors);

        const events = applyAction(world, 1, { type: "unknown_hack" } as any, new EntityManager(world));
        expect(events).toEqual([{ type: "waited", actorId: 1 }]);
    });

    it("should NOT emit wait event for throw action", () => {
        const actors = new Map<EntityId, Actor>();
        actors.set(1, { id: 1, category: "combatant", isPlayer: true, pos: { x: 0, y: 0 } } as any);
        const world = createTestWorld(actors);

        const events = applyAction(world, 1, { type: "throw" }, new EntityManager(world));
        expect(events).toEqual([]);
    });
  });

  describe("decideEnemyAction - AI Logic", () => {
      it("should path around walls", () => {
          const actors = new Map<EntityId, Actor>();
          const player = { id: 1, category: "combatant", isPlayer: true, pos: { x: 5, y: 3 }, stats: createTestStats(), energy: 0 } as any;
          const enemy = { id: 2, category: "combatant", isPlayer: false, pos: { x: 3, y: 3 }, stats: createTestStats(), energy: 0 } as any;
          actors.set(1, player);
          actors.set(2, enemy);

          const world = createTestWorld(actors);
          world.tiles[3 * 10 + 4] = 4; // Wall

          entityManager = new EntityManager(world);
          const decision = decideEnemyAction(world, enemy, player, entityManager);
          
          expect(decision.action.type).toBe("move");
      });

      it("should attack if player is adjacent", () => {
          const actors = new Map<EntityId, Actor>();
          const player = { id: 1, category: "combatant", isPlayer: true, pos: { x: 4, y: 3 }, stats: createTestStats() } as any;
          const enemy = { 
            id: 2, 
            category: "combatant", 
            isPlayer: false, 
            pos: { x: 3, y: 3 }, 
            stats: createTestStats(),
            // Set AI state to pursuing so the enemy will attack when adjacent
            aiState: "pursuing",
            lastKnownPlayerPos: { x: 4, y: 3 }
          } as any;
          actors.set(1, player);
          actors.set(2, enemy);

          const world = createTestWorld(actors);
          entityManager = new EntityManager(world);
          
          const decision = decideEnemyAction(world, enemy, player, entityManager);
          expect(decision.action).toEqual({ type: "attack", targetId: 1 });
      });

      it("should transition to alerted when spotting player", () => {
          const actors = new Map<EntityId, Actor>();
          const player = { id: 1, category: "combatant", isPlayer: true, pos: { x: 5, y: 0 }, stats: createTestStats() } as any;
          const enemy = { 
              id: 2, 
              category: "combatant", 
              isPlayer: false, 
              pos: { x: 0, y: 0 }, 
              stats: createTestStats(),
              aiState: "wandering",
              energy: 0
          } as any;
          actors.set(1, player);
          actors.set(2, enemy);
          const world = createTestWorld(actors);
          
          const decision = decideEnemyAction(world, enemy, player, new EntityManager(world));
          
          expect(enemy.aiState).toBe("alerted");
          expect(decision.justAlerted).toBe(true);
      });

      it("should transition from pursuing to searching when sight is lost", () => {
          const actors = new Map<EntityId, Actor>();
          // Player far away (unseen)
          const player = { id: 1, category: "combatant", isPlayer: true, pos: { x: 9, y: 9 }, stats: createTestStats() } as any;
          const enemy = { 
              id: 2, 
              category: "combatant", 
              isPlayer: false, 
              pos: { x: 0, y: 0 }, 
              stats: createTestStats(),
              aiState: "pursuing", // Currently pursuing
              lastKnownPlayerPos: { x: 5, y: 5 }
          } as any;
          actors.set(1, player);
          actors.set(2, enemy);
          const world = createTestWorld(actors);
          
          // Should switch to searching because can't see player
          decideEnemyAction(world, enemy, player, new EntityManager(world));
          
          expect(enemy.aiState).toBe("searching");
      });

      it("should transition from searching to alerted when sight regained", () => {
          const actors = new Map<EntityId, Actor>();
          // Player adjacent (visible)
          const player = { id: 1, category: "combatant", isPlayer: true, pos: { x: 1, y: 0 }, stats: createTestStats() } as any;
          const enemy = { 
              id: 2, 
              category: "combatant", 
              isPlayer: false, 
              pos: { x: 0, y: 0 }, 
              stats: createTestStats(),
              aiState: "searching", 
              lastKnownPlayerPos: { x: 5, y: 5 }
          } as any;
          actors.set(1, player);
          actors.set(2, enemy);
          const world = createTestWorld(actors);
          
          const decision = decideEnemyAction(world, enemy, player, new EntityManager(world));
          
          expect(enemy.aiState).toBe("alerted");
          expect(decision.justAlerted).toBe(true);
      });

      it("should transition from searching to wandering when reached target", () => {
          const actors = new Map<EntityId, Actor>();
          // Player far away (unseen) - Manhattan dist > 8
          // Enemy at 9,9. Player at 0,0. Dist = 18.
          const player = { id: 1, category: "combatant", isPlayer: true, pos: { x: 0, y: 0 }, stats: createTestStats() } as any;
          const enemy = { 
              id: 2, 
              category: "combatant", 
              isPlayer: false, 
              pos: { x: 9, y: 9 }, // At target
              stats: createTestStats(),
              aiState: "searching", 
              lastKnownPlayerPos: { x: 9, y: 9 }
          } as any;
          actors.set(1, player);
          actors.set(2, enemy);
          const world = createTestWorld(actors);
          
          decideEnemyAction(world, enemy, player, new EntityManager(world));
          
          expect(enemy.aiState).toBe("wandering");
      });
  });

  describe("stepUntilPlayerTurn", () => {
      it("should process enemy turns", () => {
          const actors = new Map<EntityId, Actor>();
          // Player is slow, enemy is fast. Enemy should move before player returns.
          const player = { id: 1, category: "combatant", isPlayer: true, pos: { x: 0, y: 0 }, speed: 10, stats: createTestStats(), energy: 0 } as any;
          const enemy = { id: 2, category: "combatant", isPlayer: false, pos: { x: 5, y: 5 }, speed: 100, stats: createTestStats(), energy: 0 } as any;
          
          actors.set(1, player);
          actors.set(2, enemy);
          const world = createTestWorld(actors);
          const em = new EntityManager(world);

          const result = stepUntilPlayerTurn(world, 1, em);
          
          // Enemy should have taken at least one action
          expect(result.events.length).toBeGreaterThan(0);
          expect(result.awaitingPlayerId).toBe(1);
      });

      it("should handle player death during enemy turn", () => {
          const actors = new Map<EntityId, Actor>();
          const player = { id: 1, category: "combatant", isPlayer: true, pos: { x: 0, y: 0 }, speed: 10, stats: createTestStats({ hp: 1 }), energy: 0 } as any;
          // Enemy that will kill player
          const enemy = { 
              id: 2, 
              category: "combatant", 
              isPlayer: false, 
              pos: { x: 1, y: 0 }, 
              speed: 100, 
              stats: createTestStats({ attack: 100 }), 
              aiState: "pursuing", 
              energy: 100 
          } as any;
          
          actors.set(1, player);
          actors.set(2, enemy);
          const world = createTestWorld(actors);
          const em = new EntityManager(world);

          const result = stepUntilPlayerTurn(world, 1, em);
          
          expect(world.actors.has(1)).toBe(false); // Player dead
          expect(result.events.some(e => e.type === "killed" && e.targetId === 1)).toBe(true);
      });
  });

  describe("Combat Mechanics - Detailed", () => {
    let mockRandom: ReturnType<typeof vi.spyOn>;

    beforeEach(() => {
        mockRandom = vi.spyOn(Math, 'random');
    });

    afterEach(() => {
        mockRandom.mockRestore();
    });

    it("should dodge attack when roll > hit chance", () => {
        const actors = new Map<EntityId, Actor>();
        // Acc 100, Eva 50. Hit Chance = 50.
        const player = { id: 1, category: "combatant", isPlayer: true, pos: { x: 3, y: 3 }, stats: createTestStats({ accuracy: 100 }), energy: 0 } as any;
        const enemy = { id: 2, category: "combatant", isPlayer: false, pos: { x: 4, y: 3 }, stats: createTestStats({ evasion: 50, hp: 10 }), energy: 0 } as any;
        
        actors.set(1, player);
        actors.set(2, enemy);
        const world = createTestWorld(actors);
        
        // Mock random to be 51 (scale 0-100 logic uses * 100) -> 0.51
        mockRandom.mockReturnValue(0.51);

        const events = applyAction(world, 1, { type: "attack", targetId: 2 }, new EntityManager(world));
        
        expect(events.some(e => e.type === "dodged")).toBe(true);
        expect(enemy.stats.hp).toBe(10); // No damage
    });

    it("should crit when roll < crit chance", () => {
        const actors = new Map<EntityId, Actor>();
        // Acc 100, Eva 0. Hit Chance = 100.
        // Crit Chance 50%.
        const player = { 
            id: 1, category: "combatant", isPlayer: true, pos: { x: 3, y: 3 }, 
            stats: createTestStats({ accuracy: 100, critChance: 50, critMultiplier: 200, attack: 10 }),
            energy: 0
        } as any;
        const enemy = { id: 2, category: "combatant", isPlayer: false, pos: { x: 4, y: 3 }, stats: createTestStats({ evasion: 0, defense: 0, hp: 50 }), energy: 0 } as any;
        
        actors.set(1, player);
        actors.set(2, enemy);
        const world = createTestWorld(actors);
        
        // Mock random:
        // 1. Hit roll: 0.1 (Hit)
        // 2. Crit roll: 0.4 (Crit, since < 0.5)
        // 3. Block roll: 0.9 (No block)
        mockRandom.mockReturnValueOnce(0.1).mockReturnValueOnce(0.4).mockReturnValueOnce(0.9);

        const events = applyAction(world, 1, { type: "attack", targetId: 2 }, new EntityManager(world));
        
        // Damage = 10 * 2 = 20
        const dmgEvent = events.find(e => e.type === "damaged") as any;
        expect(dmgEvent).toBeDefined();
        expect(dmgEvent.amount).toBe(20);
        expect(dmgEvent.isCrit).toBe(true);
    });

    it("should block when roll < block chance", () => {
        const actors = new Map<EntityId, Actor>();
        const player = { 
            id: 1, category: "combatant", isPlayer: true, pos: { x: 3, y: 3 }, 
            stats: createTestStats({ accuracy: 100, critChance: 0, attack: 10 }) 
        } as any;
        const enemy = { 
            id: 2, category: "combatant", isPlayer: false, pos: { x: 4, y: 3 }, 
            stats: createTestStats({ evasion: 0, defense: 0, hp: 50, blockChance: 50 }), energy: 0
        } as any;
        
        actors.set(1, player);
        actors.set(2, enemy);
        const world = createTestWorld(actors);
        
        // Mock random:
        // 1. Hit roll: 0.1
        // 2. Crit roll: 0.9
        // 3. Block roll: 0.4 (Block, since < 0.5)
        mockRandom.mockReturnValueOnce(0.1).mockReturnValueOnce(0.9).mockReturnValueOnce(0.4);

        const events = applyAction(world, 1, { type: "attack", targetId: 2 }, new EntityManager(world));
        
        // Damage = 10 * 0.5 = 5
        const dmgEvent = events.find(e => e.type === "damaged") as any;
        expect(dmgEvent.amount).toBe(5);
        expect(dmgEvent.isBlock).toBe(true);
    });

    it("should lifesteal on hit", () => {
        const actors = new Map<EntityId, Actor>();
        const player = { 
            id: 1, category: "combatant", isPlayer: true, pos: { x: 3, y: 3 }, 
            stats: createTestStats({ accuracy: 100, attack: 10, lifesteal: 50, hp: 10, maxHp: 20 }) 
        } as any;
        const enemy = { id: 2, category: "combatant", isPlayer: false, pos: { x: 4, y: 3 }, stats: createTestStats({ hp: 20, defense: 0 }) } as any;
        
        actors.set(1, player);
        actors.set(2, enemy);
        const world = createTestWorld(actors);

        // Standard hit
        mockRandom.mockReturnValue(0.1);

        const events = applyAction(world, 1, { type: "attack", targetId: 2 }, new EntityManager(world));
        
        // Damage 10. Heal 50% = 5. HP -> 15.
        expect(player.stats.hp).toBe(15);
        expect(events.some(e => e.type === "healed")).toBe(true);
    });

    it("should not lifesteal beyond maxHp", () => {
        const actors = new Map<EntityId, Actor>();
        const player = { 
            id: 1, category: "combatant", isPlayer: true, pos: { x: 3, y: 3 }, 
            stats: createTestStats({ accuracy: 100, attack: 10, lifesteal: 100, hp: 19, maxHp: 20 }) 
        } as any;
        const enemy = { id: 2, category: "combatant", isPlayer: false, pos: { x: 4, y: 3 }, stats: createTestStats({ hp: 20, defense: 0 }) } as any;
        
        actors.set(1, player);
        actors.set(2, enemy);
        const world = createTestWorld(actors);

        mockRandom.mockReturnValue(0.1);

        applyAction(world, 1, { type: "attack", targetId: 2 }, new EntityManager(world));
        
        // Damage 10. Heal 10. HP 19+10 = 29 > 20. Should be 20.
        expect(player.stats.hp).toBe(20);
    });
  });

  describe("Level Up Logic", () => {
      it("should level up when collecting ample experience", () => {
          const actors = new Map<EntityId, Actor>();
          const player = { 
              id: 1, category: "combatant", isPlayer: true, pos: { x: 3, y: 3 }, 
              stats: createTestStats({ level: 1, exp: 0, expToNextLevel: 100 }) 
          } as any;
          // Orb with 150 exp
          const orb = { 
              id: 2, category: "collectible", type: "exp_orb", pos: { x: 4, y: 3 }, expAmount: 150 
          } as any;
  
          actors.set(1, player);
          actors.set(2, orb);
          const world = createTestWorld(actors);
          
          // Move player onto orb
          const events = applyAction(world, 1, { type: "move", dx: 1, dy: 0 }, new EntityManager(world));
          
          expect(player.stats.level).toBe(2);
          expect(player.stats.exp).toBe(50); // 150 - 100 = 50
          expect(events.some(e => e.type === "leveled-up")).toBe(true);
      });
  });
  describe("Diagonal Mechanics", () => {
    it("should allow enemy to attack player diagonally", () => {
        const actors = new Map<EntityId, Actor>();
        // Enemy at 4,4. Player at 5,5 (diagonal)
        const enemy = { 
            id: 1, 
            category: "combatant", 
            isPlayer: false, 
            pos: { x: 4, y: 4 }, 
            stats: createTestStats(),
            aiState: "pursuing", // Skip alert phase
            energy: 0
        } as any;
        const player = { 
            id: 2, 
            category: "combatant", 
            isPlayer: true, 
            pos: { x: 5, y: 5 }, 
            stats: createTestStats(),
            energy: 0 
        } as any;
        
        actors.set(1, enemy);
        actors.set(2, player);
        const world = createTestWorld(actors);
        
        // Enemy should decide to attack
        const decision = decideEnemyAction(world, enemy, player, new EntityManager(world));
        
        expect(decision.action.type).toBe("attack");
        if (decision.action.type === "attack") {
            expect(decision.action.targetId).toBe(player.id);
        }
    });

    it("should allow player to attack enemy diagonally via applyAction", () => {
        const actors = new Map<EntityId, Actor>();
        const player = { id: 1, category: "combatant", isPlayer: true, pos: { x: 4, y: 4 }, stats: createTestStats(), energy: 0 } as any;
        const enemy = { id: 2, category: "combatant", isPlayer: false, pos: { x: 5, y: 5 }, stats: createTestStats(), energy: 0 } as any;
        
        actors.set(1, player);
        actors.set(2, enemy);
        const world = createTestWorld(actors);
        
        const action: any = { type: "attack", targetId: 2 };
        const events = applyAction(world, 1, action, new EntityManager(world));
        
        const attackEvent = events.find(e => e.type === "attacked");
        expect(attackEvent).toBeDefined();
        expect(attackEvent?.targetId).toBe(2);
    });
    
    it("should NOT generate diagonal move for enemy", () => {
        const actors = new Map<EntityId, Actor>();
        // Enemy at 4,4. Player at 4,6. Dist 2.
        const enemy = { 
            id: 1, 
            category: "combatant", 
            isPlayer: false, 
            pos: { x: 4, y: 4 }, 
            stats: createTestStats(),
            aiState: "pursuing",
            energy: 0
        } as any;
        const player = { id: 2, category: "combatant", isPlayer: true, pos: { x: 4, y: 6 }, stats: createTestStats(), energy: 0 } as any;
        
        actors.set(1, enemy);
        actors.set(2, player);
        const world = createTestWorld(actors);

        const decision = decideEnemyAction(world, enemy, player, new EntityManager(world));
        if (decision.action.type === "move") {
            const { dx, dy } = decision.action;
            // Should be (0, 1) or cardinal, sum of abs should be 1
            expect(Math.abs(dx) + Math.abs(dy)).toBe(1);
        }
    });
  });
});
