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

describe("Core: InputManager", () => {
  let inputManager;
  let camera;
  let scene;
  let domElement;
  let originalGetGamepads;

  beforeEach(() => {
    camera = new THREE.PerspectiveCamera();
    scene = new THREE.Scene();

    // Mock DOM Element
    domElement = {
      getBoundingClientRect: () => ({
        left: 0,
        top: 0,
        width: 100,
        height: 100,
      }),
      addEventListener: sinon.spy(),
      removeEventListener: sinon.spy(),
    };

    inputManager = new InputManager(camera, scene, domElement);

    // Save original navigator.getGamepads to restore later
    originalGetGamepads = navigator.getGamepads;
  });

  afterEach(() => {
    inputManager.detach();
    // Restore original navigator function
    navigator.getGamepads = originalGetGamepads;
  });

  it("CoA 1: Should attach event listeners on creation", () => {
    expect(domElement.addEventListener.calledWith("mousemove")).to.be.true;
    expect(domElement.addEventListener.calledWith("click")).to.be.true;
  });

  it("CoA 2: Should calculate normalized mouse coordinates", () => {
    // Simulate mouse event at center (50, 50)
    const event = { clientX: 50, clientY: 50 };
    inputManager.updateMouseCoords(event);

    // Center should be (0, 0) in NDC
    expect(inputManager.mouse.x).to.equal(0);
    expect(inputManager.mouse.y).to.equal(0);
  });

  it("CoA 3: Raycast should identify voxel coordinates based on intersection", () => {
    // Mock Raycaster intersection result
    // Use (0, 0.5, 0) which is the exact center of the top face of a block at (0,0,0)
    // 1x1x1 box at 0,0,0 extends from -0.5 to 0.5
    const mockIntersection = {
      object: { isInstancedMesh: true },
      point: new THREE.Vector3(0, 0.5, 0),
      face: { normal: new THREE.Vector3(0, 1, 0) }, // Up normal
    };

    // Stub the internal raycaster call
    sinon
      .stub(inputManager.raycaster, "intersectObjects")
      .returns([mockIntersection]);

    const result = inputManager.raycast();

    expect(result).to.exist;

    // Hit Logic: 0.5 + (-0.5 * 1) = 0.
    // Block hit is at 0,0,0
    expect(result.hitPosition.x).to.equal(0);
    expect(result.hitPosition.y).to.equal(0);
    expect(result.hitPosition.z).to.equal(0);

    // Target Logic: 0 + 1 = 1
    // Voxel Position (Target) should be 0,1,0 (Above the floor)
    expect(result.voxelPosition.x).to.equal(0);
    expect(result.voxelPosition.y).to.equal(1);
    expect(result.voxelPosition.z).to.equal(0);
  });

  it("CoA 4: Mouse move should update cursor and emit hover", () => {
    const hoverSpy = sinon.spy();
    inputManager.addEventListener("hover", hoverSpy);

    // Stub raycast to return a hit
    sinon.stub(inputManager, "raycast").returns({
      voxelPosition: new THREE.Vector3(1, 1, 1),
    });

    // Simulate Move
    inputManager.onMouseMove({ clientX: 0, clientY: 0 });

    expect(inputManager.cursor.visible).to.be.true;
    expect(inputManager.cursor.position.x).to.equal(1);
    expect(hoverSpy.called).to.be.true;
  });

  it("CoA 5: Mouse click should emit click event with coords", () => {
    const clickSpy = sinon.spy();
    inputManager.addEventListener("click", clickSpy);

    // Use the API method to ensure internal state (cursorPos) is synced with Mesh
    // Directly setting mesh.position might desync the internal tracker used for the event payload
    inputManager.setCursor(2, 2, 2);

    // Simulate Click
    inputManager.onMouseClick({});

    expect(clickSpy.calledOnce).to.be.true;
    const payload = clickSpy.firstCall.args[0].detail; // Access detail
    expect(payload.x).to.equal(2);
    expect(payload.y).to.equal(2);
    expect(payload.z).to.equal(2);
  });

  it("CoA 6: Keyboard events should be tracked and emitted", () => {
    const keySpy = sinon.spy();
    inputManager.addEventListener("keydown", keySpy);

    // Dispatch window event
    const event = new KeyboardEvent("keydown", { code: "KeyW" });
    window.dispatchEvent(event);

    expect(keySpy.called).to.be.true;
    expect(keySpy.firstCall.args[0].detail).to.equal("KeyW");
    expect(inputManager.isKeyPressed("KeyW")).to.be.true;

    // Key Up
    const upEvent = new KeyboardEvent("keyup", { code: "KeyW" });
    window.dispatchEvent(upEvent);

    expect(inputManager.isKeyPressed("KeyW")).to.be.false;
  });

  it("CoA 7: Gamepad button press should emit events", () => {
    const buttonSpy = sinon.spy();
    inputManager.addEventListener("gamepadbuttondown", buttonSpy);

    // Mock Gamepad State
    const mockGamepad = {
      index: 0,
      buttons: [{ pressed: true }], // Button 0 pressed
      axes: [0, 0],
    };

    // Mock Navigator directly on the window object (browser environment)
    // Note: Some browsers make navigator read-only, but usually this works in test runners.
    // If strict, we might need Object.defineProperty
    Object.defineProperty(navigator, "getGamepads", {
      value: () => [mockGamepad],
      writable: true,
    });

    // Trigger Poll
    inputManager.update();

    expect(buttonSpy.calledOnce).to.be.true;
    expect(buttonSpy.firstCall.args[0].detail).to.deep.equal({
      gamepadIndex: 0,
      buttonIndex: 0,
    });
  });

  it("CoA 8: Gamepad axis movement should emit events with deadzone", () => {
    const axisSpy = sinon.spy();
    inputManager.addEventListener("gamepadaxis", axisSpy);

    // Mock Gamepad State
    const mockGamepad = {
      index: 0,
      buttons: [],
      axes: [0.5, 0.05], // Axis 0 moved, Axis 1 inside deadzone (0.15)
    };

    Object.defineProperty(navigator, "getGamepads", {
      value: () => [mockGamepad],
      writable: true,
    });

    inputManager.update();

    expect(axisSpy.calledOnce).to.be.true; // Only Axis 0 should trigger
    expect(axisSpy.firstCall.args[0].detail.value).to.equal(0.5);
  });

  it("CoA 9: setCursor should prevent movement to invalid locations if validator provided", () => {
    // Setup a validator that only allows positive X values
    inputManager.setValidator((x, y, z) => {
      return x >= 0;
    });

    // Attempt valid move
    inputManager.setCursor(5, 1, 5);
    expect(inputManager.cursor.position.x).to.equal(5);

    // Attempt invalid move (blocked by validator)
    inputManager.setCursor(-1, 1, 5);
    // Should stay at previous valid position
    expect(inputManager.cursor.position.x).to.equal(5);
  });
});
