import { expect } from "@esm-bundle/chai";
import { SkillTreeUI } from "../../src/ui/components/SkillTreeUI.js";
import { Explorer } from "../../src/units/Explorer.js";
import vanguardDef from "../../src/assets/data/classes/vanguard.json" with {
  type: "json",
};

describe("UI: SkillTreeUI", () => {
  let element;
  let container;
  let testUnit;
  let mockTreeDef;

  beforeEach(() => {
    container = document.createElement("div");
    document.body.appendChild(container);
    element = document.createElement("skill-tree-ui");
    container.appendChild(element);

    // Create a test Explorer unit
    testUnit = new Explorer("test-unit-1", "Test Vanguard", "CLASS_VANGUARD", vanguardDef);
    testUnit.classMastery["CLASS_VANGUARD"] = {
      level: 5,
      xp: 250,
      skillPoints: 3,
      unlockedNodes: [],
    };
    testUnit.recalculateBaseStats(vanguardDef);

    // Create mock tree definition
    mockTreeDef = {
      id: "TREE_TEST",
      nodes: {
        ROOT: {
          id: "ROOT",
          tier: 1,
          type: "STAT_BOOST",
          children: ["NODE_1", "NODE_2"],
          data: { stat: "health", value: 10 },
          req: 1,
          cost: 1,
        },
        NODE_1: {
          id: "NODE_1",
          tier: 2,
          type: "ACTIVE_SKILL",
          children: ["NODE_3"],
          data: { name: "Shield Bash", id: "SKILL_SHIELD_BASH" },
          req: 2,
          cost: 1,
        },
        NODE_2: {
          id: "NODE_2",
          tier: 2,
          type: "STAT_BOOST",
          children: [],
          data: { stat: "defense", value: 5 },
          req: 2,
          cost: 1,
        },
        NODE_3: {
          id: "NODE_3",
          tier: 3,
          type: "PASSIVE_ABILITY",
          children: [],
          data: { name: "Iron Skin", id: "PASSIVE_IRON_SKIN" },
          req: 3,
          cost: 2,
        },
      },
    };
  });

  afterEach(() => {
    if (container.parentNode) {
      container.parentNode.removeChild(container);
    }
  });

  // Helper to wait for LitElement update
  async function waitForUpdate() {
    await element.updateComplete;
    await new Promise((resolve) => setTimeout(resolve, 50));
  }

  // Helper to query shadow DOM
  function queryShadow(selector) {
    return element.shadowRoot?.querySelector(selector);
  }

  function queryShadowAll(selector) {
    return element.shadowRoot?.querySelectorAll(selector) || [];
  }

  describe("CoA 1: Dynamic Rendering", () => {
    it("should render tree with variable tier depths", async () => {
      element.unit = testUnit;
      element.treeDef = mockTreeDef;
      await waitForUpdate();

      const tierRows = queryShadowAll(".tier-row");
      expect(tierRows.length).to.be.greaterThan(0);

      // Should have nodes from different tiers
      const nodes = queryShadowAll(".voxel-node");
      expect(nodes.length).to.equal(4); // ROOT, NODE_1, NODE_2, NODE_3
    });

    it("should update node states immediately when unit changes", async () => {
      element.unit = testUnit;
      element.treeDef = mockTreeDef;
      await waitForUpdate();

      // Initially, ROOT should be available (level 5 >= req 1)
      const rootNode = queryShadow('[data-node-id="ROOT"]');
      expect(rootNode).to.exist;
      expect(rootNode.classList.contains("available")).to.be.true;

      // Unlock ROOT
      testUnit.classMastery["CLASS_VANGUARD"].unlockedNodes = ["ROOT"];
      element.unit = { ...testUnit }; // Trigger update
      await waitForUpdate();

      const updatedRootNode = queryShadow('[data-node-id="ROOT"]');
      expect(updatedRootNode.classList.contains("unlocked")).to.be.true;
    });

    it("should handle tier 1 to tier 5 nodes", async () => {
      const multiTierTree = {
        id: "TREE_MULTI",
        nodes: {
          T1: { id: "T1", tier: 1, type: "STAT_BOOST", children: ["T2"], req: 1, cost: 1 },
          T2: { id: "T2", tier: 2, type: "STAT_BOOST", children: ["T3"], req: 2, cost: 1 },
          T3: { id: "T3", tier: 3, type: "STAT_BOOST", children: ["T4"], req: 3, cost: 1 },
          T4: { id: "T4", tier: 4, type: "STAT_BOOST", children: ["T5"], req: 4, cost: 1 },
          T5: { id: "T5", tier: 5, type: "STAT_BOOST", children: [], req: 5, cost: 1 },
        },
      };

      element.unit = testUnit;
      element.treeDef = multiTierTree;
      await waitForUpdate();

      const tierRows = queryShadowAll(".tier-row");
      expect(tierRows.length).to.equal(5);
    });
  });

  describe("CoA 2: Validation Feedback", () => {
    it("should show inspector with disabled button for locked node", async () => {
      element.unit = testUnit;
      element.treeDef = mockTreeDef;
      await waitForUpdate();

      // Click on NODE_3 which requires NODE_1 to be unlocked first
      const node3 = queryShadow('[data-node-id="NODE_3"]');
      node3.click();
      await waitForUpdate();

      const inspector = queryShadow(".inspector");
      expect(inspector.classList.contains("visible")).to.be.true;

      const unlockButton = queryShadow(".unlock-button");
      expect(unlockButton.hasAttribute("disabled")).to.be.true;

      const errorMessage = queryShadow(".error-message");
      expect(errorMessage).to.exist;
      expect(errorMessage.textContent).to.include("Requires");
    });

    it("should show 'Insufficient Points' for available node with 0 SP", async () => {
      testUnit.classMastery["CLASS_VANGUARD"].skillPoints = 0;
      testUnit.classMastery["CLASS_VANGUARD"].unlockedNodes = ["ROOT"];

      element.unit = testUnit;
      element.treeDef = mockTreeDef;
      await waitForUpdate();

      // Click on NODE_1 which is available but costs 1 SP
      const node1 = queryShadow('[data-node-id="NODE_1"]');
      node1.click();
      await waitForUpdate();

      const errorMessage = queryShadow(".error-message");
      expect(errorMessage).to.exist;
      expect(errorMessage.textContent).to.include("Insufficient Points");

      const unlockButton = queryShadow(".unlock-button");
      expect(unlockButton.hasAttribute("disabled")).to.be.true;
    });

    it("should enable unlock button for available node with sufficient SP", async () => {
      testUnit.classMastery["CLASS_VANGUARD"].skillPoints = 3;
      testUnit.classMastery["CLASS_VANGUARD"].unlockedNodes = ["ROOT"];

      element.unit = testUnit;
      element.treeDef = mockTreeDef;
      await waitForUpdate();

      // Click on NODE_1 which is available
      const node1 = queryShadow('[data-node-id="NODE_1"]');
      node1.click();
      await waitForUpdate();

      const unlockButton = queryShadow(".unlock-button");
      expect(unlockButton.hasAttribute("disabled")).to.be.false;
      expect(unlockButton.textContent).to.include("Unlock");
    });

    it("should show 'Unlocked' state for already unlocked nodes", async () => {
      testUnit.classMastery["CLASS_VANGUARD"].unlockedNodes = ["ROOT"];

      element.unit = testUnit;
      element.treeDef = mockTreeDef;
      await waitForUpdate();

      const rootNode = queryShadow('[data-node-id="ROOT"]');
      rootNode.click();
      await waitForUpdate();

      const unlockButton = queryShadow(".unlock-button");
      expect(unlockButton.textContent).to.include("Unlocked");
      expect(unlockButton.hasAttribute("disabled")).to.be.true;
    });
  });

  describe("CoA 3: Responsive Lines", () => {
    it("should draw connection lines between nodes", async () => {
      element.unit = testUnit;
      element.treeDef = mockTreeDef;
      await waitForUpdate();

      // Trigger connection update
      element._updateConnections();
      await waitForUpdate();

      const svg = queryShadow(".connections-overlay svg");
      expect(svg).to.exist;

      const paths = queryShadowAll(".connections-overlay svg path");
      expect(paths.length).to.be.greaterThan(0);
    });

    it("should update connection lines on resize", async () => {
      element.unit = testUnit;
      element.treeDef = mockTreeDef;
      await waitForUpdate();

      // Trigger initial connection update
      element._updateConnections();
      await waitForUpdate();

      const initialPaths = queryShadowAll(".connections-overlay svg path");
      const initialCount = initialPaths.length;
      expect(initialCount).to.be.greaterThan(0);

      // Simulate resize by changing container size
      const container = queryShadow(".tree-container");
      container.style.width = "200px";
      container.style.height = "200px";

      // Wait for ResizeObserver to trigger
      await new Promise((resolve) => setTimeout(resolve, 200));
      await waitForUpdate();

      // Paths should still exist (may have been redrawn)
      const pathsAfterResize = queryShadowAll(".connections-overlay svg path");
      expect(pathsAfterResize.length).to.equal(initialCount);
    });

    it("should style connection lines based on child node status", async () => {
      element.unit = testUnit;
      element.treeDef = mockTreeDef;
      await waitForUpdate();

      // Trigger connection update
      element._updateConnections();
      await waitForUpdate();

      // ROOT -> NODE_1 connection
      const paths = queryShadowAll(".connections-overlay svg path");
      expect(paths.length).to.be.greaterThan(0);
      
      // Verify paths exist (they should have status classes applied by _updateConnections)
      // Note: Paths may not have classes if nodes aren't rendered yet, which is acceptable
      expect(paths.length).to.be.greaterThan(0);

      // Unlock ROOT and NODE_1 by directly modifying the unit's classMastery
      testUnit.classMastery["CLASS_VANGUARD"].unlockedNodes = ["ROOT", "NODE_1"];
      // Trigger update by setting the unit property again (Lit will detect the change)
      element.unit = { ...testUnit };
      // Also increment updateTrigger to force re-render
      element.updateTrigger = (element.updateTrigger || 0) + 1;
      await waitForUpdate();

      // Trigger connection update after state change
      element._updateConnections();
      await waitForUpdate();
      await new Promise((resolve) => setTimeout(resolve, 100));

      // Connection to NODE_1 should now be unlocked style
      // (Connection from ROOT to NODE_1, where NODE_1 is now unlocked)
      const updatedPaths = queryShadowAll(".connections-overlay svg path");
      expect(updatedPaths.length).to.be.greaterThan(0);
      
      // Verify NODE_1 exists and has been updated
      const node1 = queryShadow('[data-node-id="NODE_1"]');
      expect(node1).to.exist;
      // Node should have a status class (unlocked, available, or locked)
      const node1HasStatusClass = node1.classList.contains("unlocked") || 
        node1.classList.contains("available") || 
        node1.classList.contains("locked");
      expect(node1HasStatusClass).to.be.true;
      
      // Connection styling is based on child status
      // Verify that paths have status classes and were updated
      // Paths have class "connection-line" plus status class
      const allPathClasses = Array.from(updatedPaths).map((p) => Array.from(p.classList));
      const pathHasStatusClass = allPathClasses.some((classes) =>
        classes.includes("connection-line") && 
        (classes.includes("locked") || classes.includes("available") || classes.includes("unlocked"))
      );
      expect(pathHasStatusClass).to.be.true;
    });
  });

  describe("CoA 4: Scroll Position", () => {
    it("should scroll to highest tier with available node on open", async () => {
      testUnit.classMastery["CLASS_VANGUARD"].unlockedNodes = ["ROOT", "NODE_1"];
      testUnit.classMastery["CLASS_VANGUARD"].level = 5;

      element.unit = testUnit;
      element.treeDef = mockTreeDef;
      await waitForUpdate();

      // NODE_3 should be available (tier 3, parent NODE_1 is unlocked, level 5 >= req 3)
      // The scroll should center on NODE_3
      await new Promise((resolve) => setTimeout(resolve, 200));

      const node3 = queryShadow('[data-node-id="NODE_3"]');
      expect(node3).to.exist;
      // Note: scrollIntoView behavior is hard to test in headless, but we verify the node exists
    });

    it("should handle case where no nodes are available", async () => {
      testUnit.classMastery["CLASS_VANGUARD"].level = 1;
      testUnit.classMastery["CLASS_VANGUARD"].unlockedNodes = [];

      element.unit = testUnit;
      element.treeDef = mockTreeDef;
      await waitForUpdate();

      // Should not crash, tree should still render
      const treeContainer = queryShadow(".tree-container");
      expect(treeContainer).to.exist;
    });
  });

  describe("Node Status Calculation", () => {
    it("should mark node as UNLOCKED if in unlockedNodes", async () => {
      testUnit.classMastery["CLASS_VANGUARD"].unlockedNodes = ["ROOT"];

      element.unit = testUnit;
      element.treeDef = mockTreeDef;
      await waitForUpdate();

      const rootNode = queryShadow('[data-node-id="ROOT"]');
      expect(rootNode.classList.contains("unlocked")).to.be.true;
    });

    it("should mark node as AVAILABLE if parent unlocked and level requirement met", async () => {
      testUnit.classMastery["CLASS_VANGUARD"].unlockedNodes = ["ROOT"];
      testUnit.classMastery["CLASS_VANGUARD"].level = 2;

      element.unit = testUnit;
      element.treeDef = mockTreeDef;
      await waitForUpdate();

      const node1 = queryShadow('[data-node-id="NODE_1"]');
      expect(node1.classList.contains("available")).to.be.true;
    });

    it("should mark node as LOCKED if parent not unlocked", async () => {
      testUnit.classMastery["CLASS_VANGUARD"].unlockedNodes = [];
      testUnit.classMastery["CLASS_VANGUARD"].level = 5;

      element.unit = testUnit;
      element.treeDef = mockTreeDef;
      await waitForUpdate();

      const node1 = queryShadow('[data-node-id="NODE_1"]');
      expect(node1.classList.contains("locked")).to.be.true;
    });

    it("should mark node as LOCKED if level requirement not met", async () => {
      testUnit.classMastery["CLASS_VANGUARD"].unlockedNodes = ["ROOT"];
      testUnit.classMastery["CLASS_VANGUARD"].level = 1; // Below NODE_1 req of 2

      element.unit = testUnit;
      element.treeDef = mockTreeDef;
      await waitForUpdate();

      const node1 = queryShadow('[data-node-id="NODE_1"]');
      expect(node1.classList.contains("locked")).to.be.true;
    });
  });

  describe("Inspector Footer", () => {
    it("should show inspector when node is clicked", async () => {
      element.unit = testUnit;
      element.treeDef = mockTreeDef;
      await waitForUpdate();

      const rootNode = queryShadow('[data-node-id="ROOT"]');
      rootNode.click();
      await waitForUpdate();

      const inspector = queryShadow(".inspector");
      expect(inspector.classList.contains("visible")).to.be.true;
    });

    it("should hide inspector when close button is clicked", async () => {
      element.unit = testUnit;
      element.treeDef = mockTreeDef;
      await waitForUpdate();

      const rootNode = queryShadow('[data-node-id="ROOT"]');
      rootNode.click();
      await waitForUpdate();

      const closeButton = queryShadow(".inspector-close");
      closeButton.click();
      await waitForUpdate();

      const inspector = queryShadow(".inspector");
      expect(inspector.classList.contains("visible")).to.be.false;
    });

    it("should display node information in inspector", async () => {
      element.unit = testUnit;
      element.treeDef = mockTreeDef;
      await waitForUpdate();

      const node1 = queryShadow('[data-node-id="NODE_1"]');
      node1.click();
      await waitForUpdate();

      const title = queryShadow(".inspector-title");
      expect(title.textContent).to.include("Shield Bash");
    });

    it("should dispatch unlock-request event when unlock button is clicked", async () => {
      testUnit.classMastery["CLASS_VANGUARD"].unlockedNodes = ["ROOT"];
      testUnit.classMastery["CLASS_VANGUARD"].skillPoints = 3;

      element.unit = testUnit;
      element.treeDef = mockTreeDef;
      await waitForUpdate();

      let unlockEventFired = false;
      let unlockEventDetail = null;
      element.addEventListener("unlock-request", (e) => {
        unlockEventFired = true;
        unlockEventDetail = e.detail;
      });

      const node1 = queryShadow('[data-node-id="NODE_1"]');
      node1.click();
      await waitForUpdate();

      const unlockButton = queryShadow(".unlock-button");
      unlockButton.click();
      await waitForUpdate();

      expect(unlockEventFired).to.be.true;
      expect(unlockEventDetail.nodeId).to.equal("NODE_1");
      expect(unlockEventDetail.cost).to.equal(1);
    });

    it("should update node display when updateTrigger changes", async () => {
      element.unit = testUnit;
      element.treeDef = mockTreeDef;
      await waitForUpdate();

      // Initially ROOT should be available
      const rootNode = queryShadow('[data-node-id="ROOT"]');
      expect(rootNode.classList.contains("available")).to.be.true;

      // Unlock the node in the unit
      testUnit.classMastery["CLASS_VANGUARD"].unlockedNodes = ["ROOT"];

      // Increment updateTrigger to force re-render
      element.updateTrigger = (element.updateTrigger || 0) + 1;
      await waitForUpdate();

      // Now ROOT should show as unlocked
      const updatedRootNode = queryShadow('[data-node-id="ROOT"]');
      expect(updatedRootNode.classList.contains("unlocked")).to.be.true;
    });

    it("should update inspector footer when updateTrigger changes after unlock", async () => {
      testUnit.classMastery["CLASS_VANGUARD"].skillPoints = 2;
      element.unit = testUnit;
      element.treeDef = mockTreeDef;
      await waitForUpdate();

      // Click on ROOT node
      const rootNode = queryShadow('[data-node-id="ROOT"]');
      rootNode.click();
      await waitForUpdate();

      // Initially should show "Unlock" button
      const unlockButton = queryShadow(".unlock-button");
      expect(unlockButton.textContent).to.include("Unlock");

      // Simulate unlock by updating unit and incrementing updateTrigger
      testUnit.classMastery["CLASS_VANGUARD"].unlockedNodes = ["ROOT"];
      testUnit.classMastery["CLASS_VANGUARD"].skillPoints = 1;
      element.updateTrigger = (element.updateTrigger || 0) + 1;
      await waitForUpdate();

      // Now should show "Unlocked" button
      const updatedUnlockButton = queryShadow(".unlock-button");
      expect(updatedUnlockButton.textContent).to.include("Unlocked");
      expect(updatedUnlockButton.hasAttribute("disabled")).to.be.true;
    });
  });

  describe("Voxel Node Rendering", () => {
    it("should render voxel cubes with 6 faces", async () => {
      element.unit = testUnit;
      element.treeDef = mockTreeDef;
      await waitForUpdate();

      const rootNode = queryShadow('[data-node-id="ROOT"]');
      const faces = rootNode.querySelectorAll(".cube-face");
      expect(faces.length).to.equal(6);
    });

    it("should apply correct CSS classes based on node status", async () => {
      element.unit = testUnit;
      element.treeDef = mockTreeDef;
      await waitForUpdate();

      const rootNode = queryShadow('[data-node-id="ROOT"]');
      expect(rootNode.classList.contains("available")).to.be.true;

      testUnit.classMastery["CLASS_VANGUARD"].unlockedNodes = ["ROOT"];
      element.unit = { ...testUnit };
      await waitForUpdate();

      const updatedRootNode = queryShadow('[data-node-id="ROOT"]');
      expect(updatedRootNode.classList.contains("unlocked")).to.be.true;
    });

    it("should display appropriate icons for different node types", async () => {
      element.unit = testUnit;
      element.treeDef = mockTreeDef;
      await waitForUpdate();

      const statNode = queryShadow('[data-node-id="ROOT"] .node-icon');
      expect(statNode.textContent).to.include("📈");

      const skillNode = queryShadow('[data-node-id="NODE_1"] .node-icon');
      expect(skillNode.textContent).to.include("⚔️");
    });
  });

  describe("Edge Cases", () => {
    it("should handle missing unit gracefully", async () => {
      element.unit = null;
      element.treeDef = mockTreeDef;
      await waitForUpdate();

      const placeholder = queryShadow(".placeholder");
      expect(placeholder).to.exist;
      expect(placeholder.textContent).to.include("No unit selected");
    });

    it("should handle missing tree definition gracefully", async () => {
      element.unit = testUnit;
      element.treeDef = null;
      await waitForUpdate();

      // Should fall back to mock tree or show placeholder
      const treeContainer = queryShadow(".tree-container");
      expect(treeContainer).to.exist;
    });

    it("should handle nodes without children", async () => {
      const treeWithLeafNodes = {
        id: "TREE_LEAF",
        nodes: {
          LEAF: { id: "LEAF", tier: 1, type: "STAT_BOOST", children: [], req: 1, cost: 1 },
        },
      };

      element.unit = testUnit;
      element.treeDef = treeWithLeafNodes;
      await waitForUpdate();

      // Trigger connection update (should not crash with no children)
      element._updateConnections();
      await waitForUpdate();

      const svg = queryShadow(".connections-overlay svg");
      expect(svg).to.exist;
      // Should not crash when drawing connections
    });
  });
});

