import { expect } from "@esm-bundle/chai";
import sinon from "sinon";
import * as THREE from "three";
import { GameLoop } from "../../src/core/GameLoop.js";

/**
 * Tests for CombatState.spec.js Conditions of Acceptance
 * This test suite verifies that the implementation matches the specification.
 */
describe("Combat State Specification - CoA Tests", function () {
  this.timeout(30000);

  let gameLoop;
  let container;
  let mockGameStateManager;

  beforeEach(async () => {
    container = document.createElement("div");
    document.body.appendChild(container);

    gameLoop = new GameLoop();
    gameLoop.init(container);

    // Setup mock game state manager with state tracking
    let storedCombatState = null;
    mockGameStateManager = {
      currentState: "STATE_COMBAT",
      transitionTo: sinon.stub(),
      setCombatState: sinon.stub().callsFake((state) => {
        storedCombatState = state;
      }),
      getCombatState: sinon.stub().callsFake(() => {
        return storedCombatState;
      }),
      rosterManager: {
        roster: [],
      },
    };
    gameLoop.gameStateManager = mockGameStateManager;

    // Initialize a level
    const runData = {
      seed: 12345,
      depth: 1,
      squad: [],
    };
    await gameLoop.startLevel(runData, { startAnimation: false });
  });

  afterEach(() => {
    gameLoop.stop();
    if (container.parentNode) {
      container.parentNode.removeChild(container);
    }
    if (gameLoop.renderer) {
      gameLoop.renderer.dispose();
      gameLoop.renderer.forceContextLoss();
    }
  });

  describe("TurnSystem CoA Tests", () => {
    let playerUnit1, playerUnit2, enemyUnit1;

    beforeEach(() => {
      // Create test units with different speeds
      playerUnit1 = gameLoop.unitManager.createUnit("CLASS_VANGUARD", "PLAYER");
      playerUnit1.baseStats.speed = 15; // Fast
      playerUnit1.position = { x: 5, y: 1, z: 5 };
      gameLoop.grid.placeUnit(playerUnit1, playerUnit1.position);

      playerUnit2 = gameLoop.unitManager.createUnit("CLASS_VANGUARD", "PLAYER");
      playerUnit2.baseStats.speed = 8; // Slow
      playerUnit2.position = { x: 6, y: 1, z: 5 };
      gameLoop.grid.placeUnit(playerUnit2, playerUnit2.position);

      enemyUnit1 = gameLoop.unitManager.createUnit("ENEMY_DEFAULT", "ENEMY");
      enemyUnit1.baseStats.speed = 12; // Medium
      enemyUnit1.position = { x: 10, y: 1, z: 10 };
      gameLoop.grid.placeUnit(enemyUnit1, enemyUnit1.position);
    });

    it("CoA 1: Initiative Roll - Units should be sorted by Speed (Highest First) on combat start", () => {
      // Initialize combat
      gameLoop.initializeCombatUnits();
      const allUnits = gameLoop.unitManager.getAllUnits();
      gameLoop.turnSystem.startCombat(allUnits);
      gameLoop.updateCombatState();

      const combatState = mockGameStateManager.getCombatState();
      expect(combatState).to.exist;
      expect(combatState.turnQueue).to.be.an("array");

      // Check that turn queue is sorted by initiative (which should correlate with speed)
      // Note: Current implementation uses chargeMeter, not direct speed sorting
      // This test documents the current behavior vs spec
      // turnQueue is string[] per spec, so we check that it exists and has entries
      const queue = combatState.turnQueue;
      expect(queue.length).to.be.greaterThan(0);
      // Verify all entries are strings (unit IDs)
      queue.forEach((unitId) => {
        expect(unitId).to.be.a("string");
      });
    });

    it("CoA 2: Turn Start Hygiene - AP should reset to baseAP when turn begins", () => {
      // Set up a unit with low AP
      playerUnit1.currentAP = 3;
      playerUnit1.chargeMeter = 100; // Ready to act

      // Initialize combat
      gameLoop.initializeCombatUnits();
      const allUnits = gameLoop.unitManager.getAllUnits();
      gameLoop.turnSystem.startCombat(allUnits);
      gameLoop.updateCombatState();

      // When a unit's turn starts (they're at 100 charge), they should have full AP
      // AP is calculated as: 3 + floor(speed/5)
      // With speed 15: 3 + floor(15/5) = 3 + 3 = 6
      const expectedAP = 3 + Math.floor(playerUnit1.baseStats.speed / 5);
      expect(playerUnit1.currentAP).to.equal(expectedAP);
    });

    it("CoA 2: Turn Start Hygiene - Status effects should tick (placeholder test)", () => {
      // TODO: Implement status effect ticking
      // This is a placeholder to document the requirement
      playerUnit1.statusEffects = [{ id: "poison", duration: 3, damage: 5 }];

      // When turn starts, status effects should tick
      // Currently not implemented - this test documents the gap
      expect(playerUnit1.statusEffects.length).to.be.greaterThan(0);
    });

    it("CoA 2: Turn Start Hygiene - Cooldowns should decrement (placeholder test)", () => {
      // TODO: Implement cooldown decrementing
      // This is a placeholder to document the requirement
      // Currently not implemented - this test documents the gap
      expect(true).to.be.true; // Placeholder
    });

    it("CoA 3: Cycling - endTurn() should move to next unit in queue", () => {
      gameLoop.initializeCombatUnits();
      const allUnits = gameLoop.unitManager.getAllUnits();
      gameLoop.turnSystem.startCombat(allUnits);
      gameLoop.updateCombatState();

      const initialCombatState = mockGameStateManager.getCombatState();
      const initialActiveUnitId = initialCombatState.activeUnitId;

      // End turn
      gameLoop.endTurn();

      // Verify updateCombatState was called (which recalculates queue)
      expect(mockGameStateManager.setCombatState.called).to.be.true;

      // Get the new combat state
      const newCombatState = mockGameStateManager.getCombatState();
      expect(newCombatState).to.exist;
      // The active unit should have changed (unless it's the same unit's turn again)
      // At minimum, the state should be updated
      expect(newCombatState.activeUnitId).to.exist;
    });

    it("CoA 3: Cycling - Should increment round when queue is empty", () => {
      // This test documents that round tracking should be implemented
      // Currently roundNumber exists but doesn't increment
      gameLoop.initializeCombatUnits();
      const allUnits = gameLoop.unitManager.getAllUnits();
      gameLoop.turnSystem.startCombat(allUnits);
      gameLoop.updateCombatState();

      const combatState = mockGameStateManager.getCombatState();
      expect(combatState).to.exist;
      // Check both round (spec) and roundNumber (UI alias)
      expect(combatState.round).to.exist;
      expect(combatState.roundNumber).to.exist;
      // TODO: Verify round increments when queue cycles
    });
  });

  describe("MovementSystem CoA Tests", () => {
    let playerUnit;

    beforeEach(() => {
      playerUnit = gameLoop.unitManager.createUnit("CLASS_VANGUARD", "PLAYER");
      playerUnit.baseStats.movement = 4;
      playerUnit.currentAP = 10;
      playerUnit.position = { x: 5, y: 1, z: 5 };
      gameLoop.grid.placeUnit(playerUnit, playerUnit.position);
      gameLoop.createUnitMesh(playerUnit, playerUnit.position);
    });

    it("CoA 1: Validation - Move should fail if tile is blocked/occupied", async () => {
      // Start combat with the player unit
      const allUnits = gameLoop.unitManager.getAllUnits();
      gameLoop.turnSystem.startCombat(allUnits);
      gameLoop.updateCombatState();

      // Ensure player unit is active
      const activeUnit = gameLoop.turnSystem.getActiveUnit();
      if (activeUnit && activeUnit.team !== "PLAYER") {
        // If enemy is active, end turn until player is active
        while (gameLoop.turnSystem.getActiveUnit()?.team !== "PLAYER") {
          gameLoop.endTurn();
        }
      }

      // Place another unit on target tile
      const enemyUnit = gameLoop.unitManager.createUnit(
        "ENEMY_DEFAULT",
        "ENEMY"
      );
      const occupiedPos = { x: 6, y: 1, z: 5 };
      gameLoop.grid.placeUnit(enemyUnit, occupiedPos);

      // Stub getCursorPosition without replacing the entire inputManager
      const stub1 = sinon
        .stub(gameLoop.inputManager, "getCursorPosition")
        .returns(occupiedPos);

      const initialPos = { ...playerUnit.position };
      await gameLoop.handleCombatMovement(occupiedPos);

      // Unit should not have moved
      expect(playerUnit.position.x).to.equal(initialPos.x);
      expect(playerUnit.position.z).to.equal(initialPos.z);

      // Restore stub
      stub1.restore();
    });

    it("CoA 1: Validation - Move should fail if no path exists", async () => {
      // Start combat with the player unit
      const allUnits = gameLoop.unitManager.getAllUnits();
      gameLoop.turnSystem.startCombat(allUnits);
      gameLoop.updateCombatState();

      // Ensure player unit is active
      const activeUnit = gameLoop.turnSystem.getActiveUnit();
      if (activeUnit && activeUnit.team !== "PLAYER") {
        // If enemy is active, end turn until player is active
        while (gameLoop.turnSystem.getActiveUnit()?.team !== "PLAYER") {
          gameLoop.endTurn();
        }
      }

      // Try to move to an unreachable position (far away)
      const unreachablePos = { x: 20, y: 1, z: 20 };

      // Stub getCursorPosition without replacing the entire inputManager
      const stub2 = sinon
        .stub(gameLoop.inputManager, "getCursorPosition")
        .returns(unreachablePos);

      const initialPos = { ...playerUnit.position };
      await gameLoop.handleCombatMovement(unreachablePos);

      // Unit should not have moved
      expect(playerUnit.position.x).to.equal(initialPos.x);

      // Restore stub
      stub2.restore();
    });

    it("CoA 1: Validation - Move should fail if insufficient AP", async () => {
      // Start combat with the player unit
      const allUnits = gameLoop.unitManager.getAllUnits();
      gameLoop.turnSystem.startCombat(allUnits);
      gameLoop.updateCombatState();

      // Ensure player unit is active
      const activeUnit = gameLoop.turnSystem.getActiveUnit();
      if (activeUnit && activeUnit.team !== "PLAYER") {
        // If enemy is active, end turn until player is active
        while (gameLoop.turnSystem.getActiveUnit()?.team !== "PLAYER") {
          gameLoop.endTurn();
        }
      }

      playerUnit.currentAP = 0; // No AP

      const targetPos = { x: 6, y: 1, z: 5 };

      // Stub getCursorPosition without replacing the entire inputManager
      const stub3 = sinon
        .stub(gameLoop.inputManager, "getCursorPosition")
        .returns(targetPos);

      const initialPos = { ...playerUnit.position };
      await gameLoop.handleCombatMovement(targetPos);

      // Unit should not have moved
      expect(playerUnit.position.x).to.equal(initialPos.x);

      // Restore stub
      stub3.restore();
    });

    it("CoA 2: Execution - Successful move should update Unit position", async () => {
      // Start combat with the player unit
      const allUnits = gameLoop.unitManager.getAllUnits();
      gameLoop.turnSystem.startCombat(allUnits);
      gameLoop.updateCombatState();

      // Ensure player unit is active
      const activeUnit = gameLoop.turnSystem.getActiveUnit();
      if (activeUnit && activeUnit.team !== "PLAYER") {
        // If enemy is active, end turn until player is active
        while (gameLoop.turnSystem.getActiveUnit()?.team !== "PLAYER") {
          gameLoop.endTurn();
        }
      }

      const targetPos = { x: 6, y: 1, z: 5 };

      // Stub getCursorPosition without replacing the entire inputManager
      sinon.stub(gameLoop.inputManager, "getCursorPosition").returns(targetPos);

      await gameLoop.handleCombatMovement(targetPos);

      // Restore stub
      gameLoop.inputManager.getCursorPosition.restore();

      // Unit position should be updated
      expect(playerUnit.position.x).to.equal(targetPos.x);
      expect(playerUnit.position.z).to.equal(targetPos.z);
    });

    it("CoA 2: Execution - Successful move should update VoxelGrid occupancy", async () => {
      // Start combat with the player unit
      const allUnits = gameLoop.unitManager.getAllUnits();
      gameLoop.turnSystem.startCombat(allUnits);
      gameLoop.updateCombatState();

      // Ensure player unit is active
      const activeUnit = gameLoop.turnSystem.getActiveUnit();
      if (activeUnit && activeUnit.team !== "PLAYER") {
        // If enemy is active, end turn until player is active
        while (gameLoop.turnSystem.getActiveUnit()?.team !== "PLAYER") {
          gameLoop.endTurn();
        }
      }

      const targetPos = { x: 6, y: 1, z: 5 };
      const initialPos = { ...playerUnit.position };

      // Stub getCursorPosition without replacing the entire inputManager
      sinon.stub(gameLoop.inputManager, "getCursorPosition").returns(targetPos);

      // Old position should have unit
      expect(gameLoop.grid.getUnitAt(initialPos)).to.equal(playerUnit);

      await gameLoop.handleCombatMovement(targetPos);

      // Restore stub
      gameLoop.inputManager.getCursorPosition.restore();

      // New position should have unit
      expect(gameLoop.grid.getUnitAt(targetPos)).to.equal(playerUnit);
      // Old position should be empty
      expect(gameLoop.grid.getUnitAt(initialPos)).to.be.undefined;
    });

    it("CoA 2: Execution - Successful move should deduct correct AP cost", async () => {
      // Start combat with the player unit
      const allUnits = gameLoop.unitManager.getAllUnits();
      gameLoop.turnSystem.startCombat(allUnits);
      gameLoop.updateCombatState();

      // Ensure player unit is active
      const activeUnit = gameLoop.turnSystem.getActiveUnit();
      if (activeUnit && activeUnit.team !== "PLAYER") {
        // If enemy is active, end turn until player is active
        while (gameLoop.turnSystem.getActiveUnit()?.team !== "PLAYER") {
          gameLoop.endTurn();
        }
      }

      const targetPos = { x: 6, y: 1, z: 5 };
      const initialAP = playerUnit.currentAP;

      // Stub getCursorPosition without replacing the entire inputManager
      sinon.stub(gameLoop.inputManager, "getCursorPosition").returns(targetPos);

      await gameLoop.handleCombatMovement(targetPos);

      // Restore stub
      gameLoop.inputManager.getCursorPosition.restore();

      // AP should be deducted (at least 1 for adjacent move)
      expect(playerUnit.currentAP).to.be.lessThan(initialAP);
      expect(playerUnit.currentAP).to.equal(initialAP - 1); // 1 tile = 1 AP
    });

    it("CoA 3: Path Snapping - Should move to furthest reachable tile (optional QoL)", () => {
      // This is an optional feature - document that it's not implemented
      // If user clicks far away but only has AP for 2 tiles, should move 2 tiles
      playerUnit.currentAP = 2; // Only enough for 2 tiles
      const farTargetPos = { x: 10, y: 1, z: 5 }; // 5 tiles away

      // Currently not implemented - this test documents the gap
      // The current implementation just fails if unreachable
      expect(true).to.be.true; // Placeholder
    });
  });

  describe("CombatState Interface Compliance", () => {
    let playerUnit;

    beforeEach(() => {
      // Create a unit so combat state can be generated
      playerUnit = gameLoop.unitManager.createUnit("CLASS_VANGUARD", "PLAYER");
      playerUnit.position = { x: 5, y: 1, z: 5 };
      gameLoop.grid.placeUnit(playerUnit, playerUnit.position);
    });

    it("Should track combat phase (now implemented per spec)", async () => {
      // Spec defines: phase: CombatPhase
      // Now implemented in TurnSystem
      const runData = {
        seed: 12345,
        depth: 1,
        squad: [{ id: "u1", classId: "CLASS_VANGUARD" }],
      };
      await gameLoop.startLevel(runData, { startAnimation: false });
      // Set state to deployment so finalizeDeployment works
      mockGameStateManager.currentState = "STATE_DEPLOYMENT";
      const playerUnit = gameLoop.deployUnit(
        runData.squad[0],
        gameLoop.playerSpawnZone[0]
      );
      gameLoop.finalizeDeployment();

      const combatState = mockGameStateManager.getCombatState();
      expect(combatState).to.exist;
      // Now has phase property per spec
      expect(combatState.phase).to.be.oneOf([
        "INIT",
        "TURN_START",
        "WAITING_FOR_INPUT",
        "EXECUTING_ACTION",
        "TURN_END",
        "COMBAT_END",
      ]);
    });

    it("Should track isActive flag (now implemented per spec)", async () => {
      // Spec defines isActive: boolean
      // Now implemented in TurnSystem
      const runData = {
        seed: 12345,
        depth: 1,
        squad: [{ id: "u1", classId: "CLASS_VANGUARD" }],
      };
      await gameLoop.startLevel(runData, { startAnimation: false });
      // Set state to deployment so finalizeDeployment works
      mockGameStateManager.currentState = "STATE_DEPLOYMENT";
      const playerUnit = gameLoop.deployUnit(
        runData.squad[0],
        gameLoop.playerSpawnZone[0]
      );
      gameLoop.finalizeDeployment();

      const combatState = mockGameStateManager.getCombatState();
      expect(combatState).to.exist;
      // Now has isActive property per spec
      expect(combatState.isActive).to.be.a("boolean");
      expect(combatState.isActive).to.be.true; // Combat should be active
    });

    it("Should use activeUnitId string (now implemented per spec, also has activeUnit for UI)", async () => {
      // Spec defines: activeUnitId: string | null
      // Implementation now has both: activeUnitId (spec) and activeUnit (UI compatibility)
      const runData = {
        seed: 12345,
        depth: 1,
        squad: [{ id: "u1", classId: "CLASS_VANGUARD" }],
      };
      await gameLoop.startLevel(runData, { startAnimation: false });
      // Set state to deployment so finalizeDeployment works
      mockGameStateManager.currentState = "STATE_DEPLOYMENT";
      const playerUnit = gameLoop.deployUnit(
        runData.squad[0],
        gameLoop.playerSpawnZone[0]
      );
      gameLoop.finalizeDeployment();

      const combatState = mockGameStateManager.getCombatState();
      expect(combatState).to.exist;
      // Now has both: spec-compliant activeUnitId and UI-compatible activeUnit
      expect(combatState.activeUnitId).to.be.a("string");
      expect(combatState.activeUnit).to.exist; // Still available for UI
    });

    it("Should use turnQueue as string[] (now implemented per spec, also has enrichedQueue for UI)", async () => {
      // Spec defines: turnQueue: string[]
      // Implementation now has both: turnQueue (spec) and enrichedQueue (UI compatibility)
      const runData = {
        seed: 12345,
        depth: 1,
        squad: [{ id: "u1", classId: "CLASS_VANGUARD" }],
      };
      await gameLoop.startLevel(runData, { startAnimation: false });
      // Set state to deployment so finalizeDeployment works
      mockGameStateManager.currentState = "STATE_DEPLOYMENT";
      const playerUnit = gameLoop.deployUnit(
        runData.squad[0],
        gameLoop.playerSpawnZone[0]
      );
      gameLoop.finalizeDeployment();

      const combatState = mockGameStateManager.getCombatState();
      expect(combatState).to.exist;
      // Now has spec-compliant turnQueue as string[]
      expect(combatState.turnQueue).to.be.an("array");
      if (combatState.turnQueue.length > 0) {
        // Spec requires just string IDs
        expect(combatState.turnQueue[0]).to.be.a("string"); // Spec format
        // Also has enrichedQueue for UI
        expect(combatState.enrichedQueue).to.be.an("array");
        if (combatState.enrichedQueue && combatState.enrichedQueue.length > 0) {
          expect(combatState.enrichedQueue[0]).to.have.property("unitId"); // UI format
        }
      }
    });
  });
});
