// SPDX-FileCopyrightText: 2021 Johannes Loher
// SPDX-FileCopyrightText: 2021 Oliver Rümpelein
//
// SPDX-License-Identifier: MIT

import { describe, expect, it } from "vitest";

import { evaluateCheck } from "../../src/dice/check-evaluation";

describe("evaluateCheck with no dice", () => {
  it("should throw an error.", () => {
    expect(() => evaluateCheck([], 10)).toThrow("Invalid number of dice.");
  });
});

describe("evaluateCheck with more dice than required by the checkTargetNumber", () => {
  it("should throw an error.", () => {
    expect(() => evaluateCheck([10, 10], 10)).toThrow("Invalid number of dice.");
  });
});

describe("evaluateCheck with less dice than required by the checkTargetNumber", () => {
  it("should throw an error.", () => {
    expect(() => evaluateCheck([10], 21)).toThrow("Invalid number of dice.");
  });
});

describe("evaluateCheck with a single die", () => {
  it("should assign the checkTargetNumber to the single die and be successful.", () => {
    expect(evaluateCheck([4], 12)).toEqual([{ result: 4, checkTargetNumber: 12, active: true, discarded: false }]);
  });

  it("should assign the checkTargetNumber to the single die on upper edge case and be successful.", () => {
    expect(evaluateCheck([4], 4)).toEqual([{ result: 4, checkTargetNumber: 4, active: true, discarded: false }]);
  });

  it("should assign the checkTargetNumber to the single die on lower edge case not be successful.", () => {
    expect(evaluateCheck([5], 4)).toEqual([{ result: 5, checkTargetNumber: 4, active: false, discarded: true }]);
  });

  it("should assign the checkTargetNumber to the single die and not be successful on upper edge case '19'", () => {
    expect(evaluateCheck([19], 4)).toEqual([{ result: 19, checkTargetNumber: 4, active: false, discarded: true }]);
  });

  it("should assign the checkTargetNumber to the single die and coup on '1'", () => {
    expect(evaluateCheck([1], 4)).toEqual([
      { result: 1, checkTargetNumber: 4, active: true, discarded: false, success: true, count: 4 },
    ]);
  });

  it("should assign the checkTargetNumber to the single die and fumble on '20'", () => {
    expect(evaluateCheck([20], 4)).toEqual([
      { result: 20, checkTargetNumber: 4, active: false, discarded: true, failure: true },
    ]);
  });

  it("should roll a die even when the checkTargetNumber is 0 and coup on 1", () => {
    expect(evaluateCheck([1], 0)).toEqual([
      { result: 1, checkTargetNumber: 0, active: true, discarded: false, success: true, count: 0 },
    ]);
  });

  it("should roll a die even when the checkTargetNumber is 0 and fail on values > 1 and < 20", () => {
    for (let i = 2; i < 20; i++) {
      expect(evaluateCheck([i], 0)).toEqual([{ result: i, checkTargetNumber: 0, active: false, discarded: true }]);
    }
  });

  it("should roll a die even when the checkTargetNumber is 0 and fumble on 20", () => {
    expect(evaluateCheck([20], 0)).toEqual([
      { result: 20, checkTargetNumber: 0, active: false, discarded: true, failure: true },
    ]);
  });

  it("should roll a die even when the checkTargetNumber is < 0 and coup on 1", () => {
    expect(evaluateCheck([1], -1)).toEqual([
      { result: 1, checkTargetNumber: -1, active: true, discarded: false, success: true, count: -1 },
    ]);
  });

  it("should roll a die even when the checkTargetNumber is < 0 and fail on values > 1 and < 20", () => {
    for (let i = 2; i < 20; i++) {
      expect(evaluateCheck([i], -1)).toEqual([{ result: i, checkTargetNumber: -1, active: false, discarded: true }]);
    }
  });

  it("should roll a die even when the checkTargetNumber is < 0 and fumble on 20", () => {
    expect(evaluateCheck([20], -1)).toEqual([
      { result: 20, checkTargetNumber: -1, active: false, discarded: true, failure: true },
    ]);
  });
});

describe("evaluateCheck with a single die and coup / fumble modification", () => {
  const maximumCoupResult = 2;
  const minimumFumbleResult = 19;

  it("should assign the checkTargetNumber to the single die and coup on 'maximumCoupResult'", () => {
    expect(evaluateCheck([maximumCoupResult], 4, { maximumCoupResult })).toEqual([
      {
        result: maximumCoupResult,
        checkTargetNumber: 4,
        active: true,
        discarded: false,
        success: true,
        count: 4,
      },
    ]);
  });

  it("should assign the checkTargetNumber to the single die and not coup on lower edge case 'maximumCoupResult + 1'", () => {
    expect(evaluateCheck([maximumCoupResult + 1], 4, { maximumCoupResult })).toEqual([
      { result: maximumCoupResult + 1, checkTargetNumber: 4, active: true, discarded: false },
    ]);
  });

  it("should assign the checkTargetNumber to the single die and fumble on 'minimumFumbleResultResult'", () => {
    expect(evaluateCheck([minimumFumbleResult], 20, { minimumFumbleResult })).toEqual([
      { result: minimumFumbleResult, checkTargetNumber: 20, active: true, discarded: false, failure: true },
    ]);
  });

  it("should assign the checkTargetNumber to the single die and not fumble on upper edge case 'minimumFumbleResult - 1'", () => {
    expect(evaluateCheck([minimumFumbleResult - 1], 20, { minimumFumbleResult })).toEqual([
      { result: minimumFumbleResult - 1, checkTargetNumber: 20, active: true, discarded: false },
    ]);
  });

  it("should roll a die even when the checkTargetNumber is 0 and coup on 'maximumCoupResult'", () => {
    expect(evaluateCheck([maximumCoupResult], 0, { maximumCoupResult })).toEqual([
      {
        result: maximumCoupResult,
        checkTargetNumber: 0,
        active: true,
        discarded: false,
        success: true,
        count: 0,
      },
    ]);
  });

  it("should roll a die even when the checkTargetNumber is 0 and fail on '> maximumCoupResult and < minimumFumbleResult", () => {
    for (let i = maximumCoupResult + 1; i < minimumFumbleResult; i++) {
      expect(evaluateCheck([i], 0, { maximumCoupResult, minimumFumbleResult })).toEqual([
        { result: i, checkTargetNumber: 0, active: false, discarded: true },
      ]);
    }
  });

  it("should roll a die even when the checkTargetNumber is 0 and fumble on 'minimumFumbleResult'", () => {
    expect(evaluateCheck([minimumFumbleResult], 0, { minimumFumbleResult })).toEqual([
      { result: minimumFumbleResult, checkTargetNumber: 0, active: false, discarded: true, failure: true },
    ]);
  });

  it("should roll a die even when the checkTargetNumber is < 0 and coup on 'maximumCoupResult'", () => {
    expect(evaluateCheck([maximumCoupResult], -1, { maximumCoupResult })).toEqual([
      {
        result: maximumCoupResult,
        checkTargetNumber: -1,
        active: true,
        discarded: false,
        success: true,
        count: -1,
      },
    ]);
  });

  it("should roll a die even when the checkTargetNumber is < 0 and fail on '> maximumCoupResult and < minimumFumbleResult", () => {
    for (let i = maximumCoupResult + 1; i < minimumFumbleResult; i++) {
      expect(evaluateCheck([i], -1, { maximumCoupResult, minimumFumbleResult })).toEqual([
        { result: i, checkTargetNumber: -1, active: false, discarded: true },
      ]);
    }
  });

  it("should roll a die even when the checkTargetNumber is < 0 and fumble on 'minimumFumbleResult'", () => {
    expect(evaluateCheck([minimumFumbleResult], -1, { minimumFumbleResult })).toEqual([
      { result: minimumFumbleResult, checkTargetNumber: -1, active: false, discarded: true, failure: true },
    ]);
  });
});

describe("evaluateCheck with multiple dice", () => {
  it("should assign the checkTargetNumber for the last sub check to the lowest non coup, even if the first is '20'.", () => {
    expect(evaluateCheck([20, 6, 15], 48)).toEqual([
      { result: 20, checkTargetNumber: 20, active: true, discarded: false, failure: true },
      { result: 6, checkTargetNumber: 8, active: true, discarded: false },
      { result: 15, checkTargetNumber: 20, active: true, discarded: false },
    ]);
  });

  it("should assign the checkTargetNumber for the last sub check to the first coup if there are only coups.", () => {
    expect(evaluateCheck([1, 1, 1], 48)).toEqual([
      { result: 1, checkTargetNumber: 8, active: true, discarded: false, success: true, count: 8 },
      { result: 1, checkTargetNumber: 20, active: true, discarded: false, success: true, count: 20 },
      { result: 1, checkTargetNumber: 20, active: true, discarded: false, success: true, count: 20 },
    ]);
  });

  it("should assign the checkTargetNumber for the last sub check to the first lowest die, even if it is higher than that value.", () => {
    expect(evaluateCheck([15, 15, 15], 48)).toEqual([
      { result: 15, checkTargetNumber: 8, active: false, discarded: true },
      { result: 15, checkTargetNumber: 20, active: true, discarded: false },
      { result: 15, checkTargetNumber: 20, active: true, discarded: false },
    ]);
  });

  it("should assign the checkTargetNumber for the last sub check to the first coup if its sum with the lowest non coup is high enough.", () => {
    expect(evaluateCheck([15, 15, 1], 48)).toEqual([
      { result: 15, checkTargetNumber: 20, active: true, discarded: false },
      { result: 15, checkTargetNumber: 20, active: true, discarded: false },
      { result: 1, checkTargetNumber: 8, active: true, discarded: false, success: true, count: 8 },
    ]);
  });

  it("should assign the checkTargetNumber for the last sub check to the first coup if its sum with the lowest non coup is high enough, even if the last die is '20'.", () => {
    expect(evaluateCheck([15, 1, 20], 48)).toEqual([
      { result: 15, checkTargetNumber: 20, active: true, discarded: false },
      { result: 1, checkTargetNumber: 8, active: true, discarded: false, success: true, count: 8 },
      { result: 20, checkTargetNumber: 20, active: true, discarded: false },
    ]);
  });

  it("should assign the checkTargetNumber for the last sub check to properly maximize the result when all dice are successes.", () => {
    expect(evaluateCheck([15, 4, 12], 46)).toEqual([
      { result: 15, checkTargetNumber: 20, active: true, discarded: false },
      { result: 4, checkTargetNumber: 6, active: true, discarded: false },
      { result: 12, checkTargetNumber: 20, active: true, discarded: false },
    ]);
  });

  it("should assign the checkTargetNumber for the last sub check to properly maximize the result when one dice is a failure.", () => {
    expect(evaluateCheck([15, 8, 12], 46)).toEqual([
      { result: 15, checkTargetNumber: 20, active: true, discarded: false },
      { result: 8, checkTargetNumber: 6, active: false, discarded: true },
      { result: 12, checkTargetNumber: 20, active: true, discarded: false },
    ]);
  });

  it("should assign the checkTargetNumber for the last sub check to properly maximize the result on 'lowest dice higher than last check target number and coups thrown'-edge case, coup not used for last sub CTN.", () => {
    expect(evaluateCheck([15, 1, 8], 46)).toEqual([
      { result: 15, checkTargetNumber: 20, active: true, discarded: false },
      { result: 1, checkTargetNumber: 20, active: true, discarded: false, success: true, count: 20 },
      { result: 8, checkTargetNumber: 6, active: false, discarded: true },
    ]);
  });

  it("should assign the checkTargetNumber for the last sub check to properly maximize the result on 2-dice 'lowest dice higher than last check target number and coups thrown'-edge case, coup not used for last sub CTN.", () => {
    expect(evaluateCheck([1, 8], 24)).toEqual([
      { result: 1, checkTargetNumber: 20, active: true, discarded: false, success: true, count: 20 },
      { result: 8, checkTargetNumber: 4, active: false, discarded: true },
    ]);
  });

  it("should assign the checkTargetNumber for the last sub check to properly maximize the result on 2-dice 'lowest dice higher than last check target number and coups thrown'-edge case, coup used for last sub CTN.", () => {
    expect(evaluateCheck([1, 19], 38)).toEqual([
      { result: 1, checkTargetNumber: 18, active: true, discarded: false, success: true, count: 18 },
      { result: 19, checkTargetNumber: 20, active: true, discarded: false },
    ]);
  });

  it("should assign the checkTargetNumber for the last sub check to properly maximize the result when there is more than one coup and a coup is used for the last sub CTN", () => {
    expect(evaluateCheck([1, 1, 15], 48)).toEqual([
      { result: 1, checkTargetNumber: 8, active: true, discarded: false, success: true, count: 8 },
      { result: 1, checkTargetNumber: 20, active: true, discarded: false, success: true, count: 20 },
      { result: 15, checkTargetNumber: 20, active: true, discarded: false },
    ]);
  });
});

describe("evaluateCheck with multiple dice and coup / fumble modification", () => {
  it("should assign the checkTargetNumber for the last sub check to the lowest non coup and fumble if the first is '19'.", () => {
    expect(evaluateCheck([19, 15, 6], 48, { maximumCoupResult: 2, minimumFumbleResult: 19 })).toEqual([
      { result: 19, checkTargetNumber: 20, active: true, discarded: false, failure: true },
      { result: 15, checkTargetNumber: 20, active: true, discarded: false },
      { result: 6, checkTargetNumber: 8, active: true, discarded: false },
    ]);
  });

  it("should assign the checkTargetNumber for the last sub check to the first coup if there are only coups ('1' and '2').", () => {
    expect(evaluateCheck([2, 1, 2], 48, { maximumCoupResult: 2, minimumFumbleResult: 19 })).toEqual([
      { result: 2, checkTargetNumber: 8, active: true, discarded: false, success: true, count: 8 },
      { result: 1, checkTargetNumber: 20, active: true, discarded: false, success: true, count: 20 },
      { result: 2, checkTargetNumber: 20, active: true, discarded: false, success: true, count: 20 },
    ]);
  });

  it("should assign the checkTargetNumber for the last sub check to the first lowest die, even if it is higher than that value.", () => {
    expect(evaluateCheck([15, 15, 15], 48, { maximumCoupResult: 2, minimumFumbleResult: 19 })).toEqual([
      { result: 15, checkTargetNumber: 8, active: false, discarded: true },
      { result: 15, checkTargetNumber: 20, active: true, discarded: false },
      { result: 15, checkTargetNumber: 20, active: true, discarded: false },
    ]);
  });

  it("should assign the checkTargetNumber for the last sub check to the first coup ('2') if its sum with the lowest non coup is high enough.", () => {
    expect(evaluateCheck([15, 15, 2], 48, { maximumCoupResult: 2, minimumFumbleResult: 19 })).toEqual([
      { result: 15, checkTargetNumber: 20, active: true, discarded: false },
      { result: 15, checkTargetNumber: 20, active: true, discarded: false },
      { result: 2, checkTargetNumber: 8, active: true, discarded: false, success: true, count: 8 },
    ]);
  });

  it("should assign the checkTargetNumber for the last sub check to the first coup ('2') if its sum with the lowest non coup is high enough, even if the last die is '20'.", () => {
    expect(evaluateCheck([15, 2, 20], 48, { maximumCoupResult: 2, minimumFumbleResult: 19 })).toEqual([
      { result: 15, checkTargetNumber: 20, active: true, discarded: false },
      { result: 2, checkTargetNumber: 8, active: true, discarded: false, success: true, count: 8 },
      { result: 20, checkTargetNumber: 20, active: true, discarded: false },
    ]);
  });

  it("should assign the checkTargetNumber for the last sub check to the first coup ('2') if its sum with the lowest non coup is high enough, even if the last die is '19'.", () => {
    expect(evaluateCheck([15, 2, 19], 48, { maximumCoupResult: 2, minimumFumbleResult: 19 })).toEqual([
      { result: 15, checkTargetNumber: 20, active: true, discarded: false },
      { result: 2, checkTargetNumber: 8, active: true, discarded: false, success: true, count: 8 },
      { result: 19, checkTargetNumber: 20, active: true, discarded: false },
    ]);
  });

  it("should assign the checkTargetNumber for the last sub check to properly maximize the result when all dice are successes.", () => {
    expect(evaluateCheck([15, 4, 12], 46, { maximumCoupResult: 2, minimumFumbleResult: 19 })).toEqual([
      { result: 15, checkTargetNumber: 20, active: true, discarded: false },
      { result: 4, checkTargetNumber: 6, active: true, discarded: false },
      { result: 12, checkTargetNumber: 20, active: true, discarded: false },
    ]);
  });

  it("should assign the checkTargetNumber for the last sub check to properly maximize the result when one dice is a failure.", () => {
    expect(evaluateCheck([15, 8, 12], 46, { maximumCoupResult: 2, minimumFumbleResult: 19 })).toEqual([
      { result: 15, checkTargetNumber: 20, active: true, discarded: false },
      { result: 8, checkTargetNumber: 6, active: false, discarded: true },
      { result: 12, checkTargetNumber: 20, active: true, discarded: false },
    ]);
  });

  it("should assign the checkTargetNumber for the last sub check to properly maximize the result on 'lowest dice higher than last check target number and coups ('2') thrown'-edge case, coup not used for last sub CTN.", () => {
    expect(evaluateCheck([15, 2, 8], 46, { maximumCoupResult: 2 })).toEqual([
      { result: 15, checkTargetNumber: 20, active: true, discarded: false },
      { result: 2, checkTargetNumber: 20, active: true, discarded: false, success: true, count: 20 },
      { result: 8, checkTargetNumber: 6, active: false, discarded: true },
    ]);
  });

  it("should assign the checkTargetNumber for the last sub check to properly maximize the result on 2-dice 'lowest dice higher than last check target number and coups ('2') thrown'-edge case, coup not used for last sub CTN.", () => {
    expect(evaluateCheck([2, 8], 24, { maximumCoupResult: 2 })).toEqual([
      { result: 2, checkTargetNumber: 20, active: true, discarded: false, success: true, count: 20 },
      { result: 8, checkTargetNumber: 4, active: false, discarded: true },
    ]);
  });

  it("should assign the checkTargetNumber for the last sub check to properly maximize the result on 2-dice 'lowest dice higher than last check target number and coups ('2') thrown'-edge case, coup used for last sub CTN.", () => {
    expect(evaluateCheck([2, 19], 38, { maximumCoupResult: 2 })).toEqual([
      { result: 2, checkTargetNumber: 18, active: true, discarded: false, success: true, count: 18 },
      { result: 19, checkTargetNumber: 20, active: true, discarded: false },
    ]);
  });

  it("should assign the checkTargetNumber for the last sub check to properly maximize the result when there is more than one coup ('1' and '2') and a coup is used for the last sub CTN", () => {
    expect(evaluateCheck([1, 2, 15], 48, { maximumCoupResult: 2 })).toEqual([
      { result: 1, checkTargetNumber: 8, active: true, discarded: false, success: true, count: 8 },
      { result: 2, checkTargetNumber: 20, active: true, discarded: false, success: true, count: 20 },
      { result: 15, checkTargetNumber: 20, active: true, discarded: false },
    ]);
  });

  it("should use all the dice if they are coups, even if they are higher than the checkTargetNumber", () => {
    expect(evaluateCheck([18, 19, 17], 48, { maximumCoupResult: 19 })).toEqual([
      { result: 18, checkTargetNumber: 8, active: true, discarded: false, success: true, count: 8 },
      { result: 19, checkTargetNumber: 20, active: true, discarded: false, success: true, count: 20 },
      { result: 17, checkTargetNumber: 20, active: true, discarded: false, success: true, count: 20 },
    ]);
  });
});
