/**
 * Dice Rolling Module Tests
 *
 * Tests for the Vagabond RPG dice rolling system.
 * Covers d20 checks, skill/attack/save rolls, damage, and special dice mechanics.
 */

import {
  d20Check,
  skillCheck,
  attackCheck,
  saveRoll,
  damageRoll,
  doubleDice,
  countdownRoll,
  moraleCheck,
} from "../dice/_module.mjs";

/**
 * Register dice tests with Quench
 * @param {Quench} quenchRunner - The Quench test runner instance
 */
export function registerDiceTests(quenchRunner) {
  quenchRunner.registerBatch(
    "vagabond.dice.d20check",
    (context) => {
      const { describe, it, expect } = context;

      describe("d20Check Basic Functionality", () => {
        it("returns a roll result object with expected properties", async () => {
          /**
           * d20Check should return a structured result with roll object,
           * total, success boolean, crit/fumble flags, and details.
           */
          const result = await d20Check({ difficulty: 10 });

          expect(result).to.have.property("roll");
          expect(result).to.have.property("total");
          expect(result).to.have.property("success");
          expect(result).to.have.property("isCrit");
          expect(result).to.have.property("isFumble");
          expect(result).to.have.property("d20Result");
          expect(result).to.have.property("difficulty");
          expect(result.difficulty).to.equal(10);
        });

        it("determines success when total >= difficulty", async () => {
          /**
           * A roll succeeds when the total (d20 + modifiers) meets or
           * exceeds the difficulty number.
           */
          // Run multiple times to get statistical coverage
          let successCount = 0;
          let failCount = 0;

          for (let i = 0; i < 20; i++) {
            const result = await d20Check({ difficulty: 10 });
            if (result.success) {
              expect(result.total).to.be.at.least(10);
              successCount++;
            } else {
              expect(result.total).to.be.below(10);
              failCount++;
            }
          }

          // With DC 10, we should see both successes and failures
          // (statistically very likely over 20 rolls)
          expect(successCount + failCount).to.equal(20);
        });

        it("detects critical hits at or above crit threshold", async () => {
          /**
           * A critical hit occurs when the natural d20 result (before modifiers)
           * meets or exceeds the critThreshold. Default is 20.
           */
          const result = await d20Check({ difficulty: 10, critThreshold: 20 });

          // isCrit should be true only if d20Result >= critThreshold
          if (result.isCrit) {
            expect(result.d20Result).to.be.at.least(20);
          } else {
            expect(result.d20Result).to.be.below(20);
          }
        });

        it("supports lowered crit thresholds", async () => {
          /**
           * Class features like Fighter's Valor can lower the crit threshold.
           * A critThreshold of 18 means crits on 18, 19, or 20.
           */
          const result = await d20Check({ difficulty: 10, critThreshold: 18 });

          if (result.isCrit) {
            expect(result.d20Result).to.be.at.least(18);
          }
        });

        it("detects fumbles on natural 1", async () => {
          /**
           * A fumble occurs when the natural d20 shows a 1.
           * This is independent of success/failure.
           */
          const result = await d20Check({ difficulty: 10 });

          if (result.isFumble) {
            expect(result.d20Result).to.equal(1);
          }
        });
      });

      describe("Favor and Hinder Modifiers", () => {
        it("adds +d6 when favorHinder is positive", async () => {
          /**
           * Favor adds a bonus d6 to the roll total.
           * The formula becomes "1d20 + 1d6".
           */
          const result = await d20Check({ difficulty: 10, favorHinder: 1 });

          expect(result.details.favorHinder).to.equal(1);
          expect(result.favorDie).to.be.at.least(1);
          expect(result.favorDie).to.be.at.most(6);
          expect(result.details.formula).to.include("+ 1d6");
        });

        it("subtracts d6 when favorHinder is negative", async () => {
          /**
           * Hinder subtracts a d6 from the roll total.
           * The formula becomes "1d20 - 1d6".
           */
          const result = await d20Check({ difficulty: 10, favorHinder: -1 });

          expect(result.details.favorHinder).to.equal(-1);
          expect(result.favorDie).to.be.at.most(-1);
          expect(result.favorDie).to.be.at.least(-6);
          expect(result.details.formula).to.include("- 1d6");
        });

        it("has no extra die when favorHinder is 0", async () => {
          /**
           * When favor and hinder cancel out (net 0), no d6 is added.
           */
          const result = await d20Check({ difficulty: 10, favorHinder: 0 });

          expect(result.favorDie).to.equal(0);
          expect(result.details.formula).to.not.include("d6");
        });
      });

      describe("Flat Modifiers", () => {
        it("applies positive modifiers to the roll", async () => {
          /**
           * Situational modifiers are added to the roll total.
           */
          const result = await d20Check({ difficulty: 10, modifier: 5 });

          expect(result.details.modifier).to.equal(5);
          expect(result.details.formula).to.include("+ 5");
        });

        it("applies negative modifiers to the roll", async () => {
          /**
           * Negative modifiers subtract from the roll total.
           */
          const result = await d20Check({ difficulty: 10, modifier: -3 });

          expect(result.details.modifier).to.equal(-3);
          expect(result.details.formula).to.include("- 3");
        });
      });
    },
    { displayName: "Vagabond: d20 Check System" }
  );

  quenchRunner.registerBatch(
    "vagabond.dice.skillcheck",
    (context) => {
      const { describe, it, expect, beforeEach, afterEach } = context;

      let testActor = null;

      beforeEach(async () => {
        testActor = await Actor.create({
          name: "Test Skill Roller",
          type: "character",
          system: {
            stats: {
              might: { value: 4 },
              dexterity: { value: 5 },
              awareness: { value: 3 },
              reason: { value: 6 },
              presence: { value: 3 },
              luck: { value: 2 },
            },
            skills: {
              arcana: { trained: true, critThreshold: 20 },
              brawl: { trained: false, critThreshold: 20 },
              sneak: { trained: true, critThreshold: 19 },
            },
            level: 1,
          },
        });
      });

      afterEach(async () => {
        if (testActor) {
          await testActor.delete();
          testActor = null;
        }
      });

      describe("Skill Check Rolls", () => {
        it("uses correct difficulty for trained skills", async () => {
          /**
           * Trained skill difficulty = 20 - (stat × 2)
           * Arcana uses Reason (6), so difficulty = 20 - 12 = 8
           */
          const result = await skillCheck(testActor, "arcana");

          // Difficulty should be calculated as 20 - (6 × 2) = 8
          expect(result.difficulty).to.equal(8);
        });

        it("uses correct difficulty for untrained skills", async () => {
          /**
           * Untrained skill difficulty = 20 - stat
           * Brawl (untrained) uses Might (4), so difficulty = 20 - 4 = 16
           */
          const result = await skillCheck(testActor, "brawl");

          // Difficulty should be calculated as 20 - 4 = 16
          expect(result.difficulty).to.equal(16);
        });

        it("uses provided difficulty when passed in options", async () => {
          /**
           * When difficulty is passed via options (e.g., from the dialog),
           * it should be used instead of calculating from stats.
           * This ensures the dialog-displayed difficulty matches the roll.
           */
          const result = await skillCheck(testActor, "arcana", { difficulty: 12 });

          // Should use the provided difficulty, not calculate it
          expect(result.difficulty).to.equal(12);
        });

        it("uses provided critThreshold when passed in options", async () => {
          /**
           * When critThreshold is passed via options (e.g., from the dialog),
           * it should override the skill's default critThreshold.
           */
          const result = await skillCheck(testActor, "arcana", { critThreshold: 18 });

          expect(result.critThreshold).to.equal(18);
        });

        it("uses skill-specific crit threshold", async () => {
          /**
           * Skills can have modified crit thresholds from class features.
           * Sneak has critThreshold: 19 set in test data.
           */
          const result = await skillCheck(testActor, "sneak");

          expect(result.critThreshold).to.equal(19);
        });

        it("throws error for unknown skill", async () => {
          /**
           * Attempting to roll an unknown skill should throw an error.
           */
          try {
            await skillCheck(testActor, "nonexistent");
            expect.fail("Should have thrown an error");
          } catch (error) {
            expect(error.message).to.include("Unknown skill");
          }
        });
      });
    },
    { displayName: "Vagabond: Skill Check System" }
  );

  quenchRunner.registerBatch(
    "vagabond.dice.attackcheck",
    (context) => {
      const { describe, it, expect, beforeEach, afterEach } = context;

      let testActor = null;

      beforeEach(async () => {
        testActor = await Actor.create({
          name: "Test Attacker",
          type: "character",
          system: {
            stats: {
              might: { value: 5 },
              dexterity: { value: 4 },
              awareness: { value: 3 },
              reason: { value: 2 },
              presence: { value: 2 },
              luck: { value: 2 },
            },
            attacks: {
              melee: { critThreshold: 19 },
              ranged: { critThreshold: 20 },
              finesse: { critThreshold: 20 },
              brawl: { critThreshold: 20 },
            },
            level: 1,
          },
          items: [
            {
              name: "Test Sword",
              type: "weapon",
              system: {
                damage: "1d8",
                attackType: "melee",
                grip: "1h",
                damageType: "slashing",
                equipped: true,
              },
            },
          ],
        });
      });

      afterEach(async () => {
        if (testActor) await testActor.delete();
        testActor = null;
      });

      describe("Attack Check Rolls", () => {
        it("calculates difficulty from attack stat", async () => {
          /**
           * Attack difficulty = 20 - (stat × 2) (attacks are always trained)
           * Melee uses Might (5), so difficulty = 20 - 10 = 10
           */
          const testWeapon = testActor.items.find((i) => i.type === "weapon");
          const result = await attackCheck(testActor, testWeapon);

          expect(result.difficulty).to.equal(10);
        });

        it("uses attack-specific crit threshold", async () => {
          /**
           * Attack types can have modified crit thresholds.
           * Melee attacks have critThreshold: 19 in test data.
           */
          const testWeapon = testActor.items.find((i) => i.type === "weapon");
          const result = await attackCheck(testActor, testWeapon);

          expect(result.critThreshold).to.equal(19);
        });
      });
    },
    { displayName: "Vagabond: Attack Check System" }
  );

  quenchRunner.registerBatch(
    "vagabond.dice.saveroll",
    (context) => {
      const { describe, it, expect, beforeEach, afterEach } = context;

      let testActor = null;

      beforeEach(async () => {
        testActor = await Actor.create({
          name: "Test Saver",
          type: "character",
          system: {
            stats: {
              might: { value: 4 },
              dexterity: { value: 5 },
              awareness: { value: 3 },
              reason: { value: 4 },
              presence: { value: 3 },
              luck: { value: 2 },
            },
            level: 1,
          },
        });
      });

      afterEach(async () => {
        if (testActor) await testActor.delete();
        testActor = null;
      });

      describe("Save Rolls", () => {
        it("rolls against provided difficulty", async () => {
          /**
           * Save rolls use an externally provided difficulty
           * (typically from the attacker's stat).
           */
          const result = await saveRoll(testActor, "reflex", 12);

          expect(result.difficulty).to.equal(12);
        });

        it("saves do not crit (threshold stays 20)", async () => {
          /**
           * Save rolls use the default crit threshold of 20.
           * Unlike attacks, saves cannot have lowered crit thresholds.
           */
          const result = await saveRoll(testActor, "endure", 10);

          expect(result.critThreshold).to.equal(20);
        });
      });
    },
    { displayName: "Vagabond: Save Roll System" }
  );

  quenchRunner.registerBatch(
    "vagabond.dice.damage",
    (context) => {
      const { describe, it, expect } = context;

      describe("Damage Rolls", () => {
        it("evaluates damage formula", async () => {
          /**
           * Damage rolls evaluate a dice formula and return the total.
           */
          const roll = await damageRoll("2d6");

          expect(roll.total).to.be.at.least(2);
          expect(roll.total).to.be.at.most(12);
        });

        it("doubles dice on critical hit", async () => {
          /**
           * Critical hits double the number of dice rolled.
           * "2d6" becomes "4d6" on a crit.
           */
          const roll = await damageRoll("2d6", { isCrit: true });

          // 4d6 range: 4-24
          expect(roll.total).to.be.at.least(4);
          expect(roll.total).to.be.at.most(24);
        });

        it("does not double modifiers on crit", async () => {
          /**
           * Only dice are doubled on crit, not flat modifiers.
           * "1d6+3" becomes "2d6+3" (not "2d6+6").
           */
          const roll = await damageRoll("1d6+3", { isCrit: true });

          // 2d6+3 range: 5-15
          expect(roll.total).to.be.at.least(5);
          expect(roll.total).to.be.at.most(15);
        });
      });

      describe("doubleDice Helper", () => {
        it("doubles dice count in formula", () => {
          /**
           * The doubleDice helper doubles the number of each die type.
           */
          expect(doubleDice("1d6")).to.equal("2d6");
          expect(doubleDice("2d8")).to.equal("4d8");
          expect(doubleDice("3d10")).to.equal("6d10");
        });

        it("preserves modifiers when doubling dice", () => {
          /**
           * Flat modifiers should remain unchanged.
           */
          expect(doubleDice("1d6+3")).to.equal("2d6+3");
          expect(doubleDice("2d8-2")).to.equal("4d8-2");
        });

        it("handles multiple dice types", () => {
          /**
           * Formulas with multiple dice types should double each.
           */
          expect(doubleDice("1d6+1d4")).to.equal("2d6+2d4");
        });
      });
    },
    { displayName: "Vagabond: Damage Roll System" }
  );

  quenchRunner.registerBatch(
    "vagabond.dice.countdown",
    (context) => {
      const { describe, it, expect } = context;

      describe("Countdown Dice", () => {
        it("rolls the specified die size", async () => {
          /**
           * Countdown dice start as d6 and shrink to d4.
           * The result should be within the die's range.
           */
          const result = await countdownRoll(6);

          expect(result.roll).to.not.be.null;
          expect(result.result).to.be.at.least(1);
          expect(result.result).to.be.at.most(6);
        });

        it("continues on high rolls (3-6 on d6)", async () => {
          /**
           * When rolling 3+ on the countdown die, the effect continues
           * with the same die size.
           */
          // Run multiple times to test the logic
          for (let i = 0; i < 10; i++) {
            const result = await countdownRoll(6);

            if (result.result >= 3) {
              expect(result.continues).to.equal(true);
              expect(result.nextDie).to.equal(6);
              expect(result.ended).to.equal(false);
              expect(result.shrunk).to.equal(false);
            }
          }
        });

        it("shrinks die on low rolls (1-2)", async () => {
          /**
           * Rolling 1-2 on the countdown die causes it to shrink.
           * d6 → d4, d4 → effect ends.
           */
          for (let i = 0; i < 20; i++) {
            const result = await countdownRoll(6);

            if (result.result <= 2) {
              expect(result.nextDie).to.equal(4);
              expect(result.shrunk).to.equal(true);
              expect(result.ended).to.equal(false);
              break;
            }
          }
        });

        it("ends effect when d4 rolls 1-2", async () => {
          /**
           * When a d4 countdown die rolls 1-2, the effect ends completely.
           */
          for (let i = 0; i < 20; i++) {
            const result = await countdownRoll(4);

            if (result.result <= 2) {
              expect(result.nextDie).to.equal(0);
              expect(result.ended).to.equal(true);
              expect(result.continues).to.equal(false);
              break;
            }
          }
        });

        it("returns ended state for die size 0", async () => {
          /**
           * If passed a die size of 0, the effect has already ended.
           */
          const result = await countdownRoll(0);

          expect(result.roll).to.be.null;
          expect(result.continues).to.equal(false);
          expect(result.ended).to.equal(true);
        });
      });
    },
    { displayName: "Vagabond: Countdown Dice System" }
  );

  quenchRunner.registerBatch(
    "vagabond.dice.morale",
    (context) => {
      const { describe, it, expect, beforeEach, afterEach } = context;

      let testNPC = null;

      beforeEach(async () => {
        testNPC = await Actor.create({
          name: "Test Goblin",
          type: "npc",
          system: {
            hd: 1,
            hp: { value: 4, max: 4 },
            tl: 0.8,
            morale: 6,
            zone: "frontline",
          },
        });
      });

      afterEach(async () => {
        if (testNPC) await testNPC.delete();
        testNPC = null;
      });

      describe("Morale Checks", () => {
        it("rolls 2d6 against morale score", async () => {
          /**
           * Morale check: 2d6 vs Morale score.
           * Pass if roll <= morale, fail if roll > morale.
           */
          const result = await moraleCheck(testNPC);

          expect(result.roll).to.not.be.null;
          expect(result.total).to.be.at.least(2);
          expect(result.total).to.be.at.most(12);
          expect(result.morale).to.equal(6);
        });

        it("passes when roll <= morale", async () => {
          /**
           * NPC holds their ground when 2d6 <= morale.
           */
          const result = await moraleCheck(testNPC);

          if (result.total <= 6) {
            expect(result.passed).to.equal(true);
            expect(result.fled).to.equal(false);
          }
        });

        it("fails when roll > morale", async () => {
          /**
           * NPC flees when 2d6 > morale.
           */
          const result = await moraleCheck(testNPC);

          if (result.total > 6) {
            expect(result.passed).to.equal(false);
            expect(result.fled).to.equal(true);
          }
        });

        it("throws error for non-NPC actors", async () => {
          /**
           * Only NPCs can make morale checks.
           */
          const pcActor = await Actor.create({
            name: "Test PC",
            type: "character",
            system: { level: 1 },
          });

          try {
            await moraleCheck(pcActor);
            expect.fail("Should have thrown an error");
          } catch (error) {
            expect(error.message).to.include("only for NPCs");
          } finally {
            await pcActor.delete();
          }
        });
      });
    },
    { displayName: "Vagabond: Morale Check System" }
  );
}
