import { Player } from "@player";
import {
  generateContract,
  generateDummyContract,
  generateRandomContract,
  generateRandomContractOnHome,
  getRandomFilename,
  getRandomReward,
} from "../../../src/CodingContract/ContractGenerator";
import { CodingContractName, CompanyName, JobField, JobName } from "../../../src/Enums";
import { GetAllServers } from "../../../src/Server/AllServers";
import type { BaseServer } from "../../../src/Server/BaseServer";
import { SpecialServers } from "../../../src/Server/data/SpecialServers";
import { getNS, initGameEnvironment, setupBasicTestingEnvironment } from "../Utilities";
import { prestigeSourceFile } from "../../../src/Prestige";
import { CodingContractRewardType } from "../../../src/CodingContract/Contract";
import { joinFaction } from "../../../src/Faction/FactionHelpers";
import { Factions } from "../../../src/Faction/Factions";
import { Companies } from "../../../src/Company/Companies";
import { CodingContractTypes } from "../../../src/CodingContract/ContractTypes";
import { getRecordEntries } from "../../../src/Types/Record";

beforeAll(() => {
  initGameEnvironment();
});

beforeEach(() => {
  setupBasicTestingEnvironment();
  assertNumberOfContracts(0, GetAllServers());
});

function assertNumberOfContracts(expected: number, servers: BaseServer[]): void {
  expect(servers.reduce((sum, server) => sum + server.contracts.length, 0)).toStrictEqual(expected);
}

describe("Generator", () => {
  test("generateRandomContract", () => {
    generateRandomContract();
    assertNumberOfContracts(1, GetAllServers());
  });
  test("generateRandomContractOnHome", () => {
    generateRandomContractOnHome();
    assertNumberOfContracts(1, [Player.getHomeComputer()]);
  });
  test("generateDummyContract", () => {
    generateDummyContract(CodingContractName.FindLargestPrimeFactor, Player.getHomeComputer());
    assertNumberOfContracts(1, GetAllServers());
  });
  // generateContract is flexible. All properties in IGenerateContractParams are optional. This test checks the usage in
  // CodingContractsDev.tsx.
  test("generateContract - home", () => {
    generateContract({ problemType: CodingContractName.FindLargestPrimeFactor, server: SpecialServers.Home });
    assertNumberOfContracts(1, [Player.getHomeComputer()]);
  });
  // This test checks the case in which we randomize everything (e.g., problemType, server).
  test("generateContract - random server", () => {
    generateContract({});
    assertNumberOfContracts(1, GetAllServers());
  });
});

describe("getRandomFilename", () => {
  test("Check format and collision", () => {
    const server = Player.getHomeComputer();
    const set = new Set();
    // Contract names contain only alphanumeric chars.
    const regex = /[^a-zA-Z0-9]/g;
    const maxIter = 1000;
    for (let i = 0; i < maxIter; ++i) {
      const filename = getRandomFilename(server);
      if (filename == null) {
        throw new Error("Cannot generate random filename");
      }
      expect(filename).toMatch(regex);
      set.add(filename);
    }
    // getRandomFilename had a bug that made the filenames collide much easier than what we expected. Please check
    // https://github.com/bitburner-official/bitburner-src/pull/2399 for more information.
    // This test is designed to catch that kind of bug. The probability of collision is greater than zero, so this test
    // may give a false positive. However, the probability is very low (0.000008802741646607437), and the error log
    // should point that out immediately.
    expect(set.size).toStrictEqual(maxIter);
  });
});

describe("getRandomReward", () => {
  test("Disable money reward when CodingContractMoney BN multiplier is 0", () => {
    Player.bitNodeN = 8;
    prestigeSourceFile(true);
    for (let i = 0; i < 1000; ++i) {
      expect(getRandomReward().type).not.toStrictEqual(CodingContractRewardType.Money);
    }
  });
  test("Have all valid reward types", () => {
    const rewardTypeCounts = [0, 0, 0, 0];
    for (let i = 0; i < 1000; ++i) {
      ++rewardTypeCounts[getRandomReward().type];
    }
    expect(rewardTypeCounts.length).toStrictEqual(4);
    expect(rewardTypeCounts[CodingContractRewardType.FactionReputation]).toBeGreaterThan(0);
    expect(rewardTypeCounts[CodingContractRewardType.FactionReputationAll]).toBeGreaterThan(0);
    expect(rewardTypeCounts[CodingContractRewardType.CompanyReputation]).toBeGreaterThan(0);
    expect(rewardTypeCounts[CodingContractRewardType.Money]).toBeGreaterThan(0);
  });
});

describe("Receive correct reward", () => {
  test("FactionReputation", () => {
    const ns = getNS();
    generateContract({
      problemType: CodingContractName.FindLargestPrimeFactor,
      server: SpecialServers.Home,
      reward: { type: CodingContractRewardType.FactionReputation },
    });
    joinFaction(Factions.CyberSec);
    expect(Factions.CyberSec.playerReputation).toStrictEqual(0);
    const contract = Player.getHomeComputer().contracts[0];
    ns.codingcontract.attempt(contract.getAnswer(), contract.fn);
    expect(Factions.CyberSec.playerReputation).toBeGreaterThan(0);
  });
  test("FactionReputationAll", () => {
    const ns = getNS();
    generateContract({
      problemType: CodingContractName.FindLargestPrimeFactor,
      server: SpecialServers.Home,
      reward: { type: CodingContractRewardType.FactionReputationAll },
    });
    joinFaction(Factions.CyberSec);
    joinFaction(Factions.NiteSec);
    expect(Factions.CyberSec.playerReputation).toStrictEqual(0);
    expect(Factions.NiteSec.playerReputation).toStrictEqual(0);
    const contract = Player.getHomeComputer().contracts[0];
    ns.codingcontract.attempt(contract.getAnswer(), contract.fn);
    expect(Factions.CyberSec.playerReputation).toBeGreaterThan(0);
    expect(Factions.NiteSec.playerReputation).toBeGreaterThan(0);
  });
  test("CompanyReputation", () => {
    const ns = getNS();
    generateContract({
      problemType: CodingContractName.FindLargestPrimeFactor,
      server: SpecialServers.Home,
      reward: { type: CodingContractRewardType.CompanyReputation },
    });
    expect(ns.singularity.applyToCompany(CompanyName.JoesGuns, JobField.employee)).toStrictEqual(JobName.employee);
    expect(Companies[CompanyName.JoesGuns].playerReputation).toStrictEqual(0);
    const contract = Player.getHomeComputer().contracts[0];
    ns.codingcontract.attempt(contract.getAnswer(), contract.fn);
    expect(Companies[CompanyName.JoesGuns].playerReputation).toBeGreaterThan(0);
  });
  test("Money", () => {
    const ns = getNS();
    generateContract({
      problemType: CodingContractName.FindLargestPrimeFactor,
      server: SpecialServers.Home,
      reward: { type: CodingContractRewardType.Money },
    });
    Player.money = 0;
    const contract = Player.getHomeComputer().contracts[0];
    ns.codingcontract.attempt(contract.getAnswer(), contract.fn);
    expect(Player.money).toBeGreaterThan(0);
  });
});

describe("Receive fallback reward", () => {
  test("FactionReputation", () => {
    const ns = getNS();
    generateContract({
      problemType: CodingContractName.FindLargestPrimeFactor,
      server: SpecialServers.Home,
      reward: { type: CodingContractRewardType.FactionReputation },
    });
    expect(Player.factions.length).toStrictEqual(0);
    Player.money = 0;
    const contract = Player.getHomeComputer().contracts[0];
    ns.codingcontract.attempt(contract.getAnswer(), contract.fn);
    expect(Player.money).toBeGreaterThan(0);
  });
  test("FactionReputationAll", () => {
    const ns = getNS();
    generateContract({
      problemType: CodingContractName.FindLargestPrimeFactor,
      server: SpecialServers.Home,
      reward: { type: CodingContractRewardType.FactionReputationAll },
    });
    expect(Player.factions.length).toStrictEqual(0);
    Player.money = 0;
    const contract = Player.getHomeComputer().contracts[0];
    ns.codingcontract.attempt(contract.getAnswer(), contract.fn);
    expect(Player.money).toBeGreaterThan(0);
  });
  test("CompanyReputation", () => {
    const ns = getNS();
    generateContract({
      problemType: CodingContractName.FindLargestPrimeFactor,
      server: SpecialServers.Home,
      reward: { type: CodingContractRewardType.CompanyReputation },
    });
    expect(Object.keys(Player.jobs).length).toStrictEqual(0);
    joinFaction(Factions.CyberSec);
    expect(Factions.CyberSec.playerReputation).toStrictEqual(0);
    const contract = Player.getHomeComputer().contracts[0];
    ns.codingcontract.attempt(contract.getAnswer(), contract.fn);
    expect(Factions.CyberSec.playerReputation).toBeGreaterThan(0);
  });
});

describe("Check getAnswer and solver", () => {
  // These contracts do not give a way to generate the answer.
  const skippedContractTypes = [
    CodingContractName.Proper2ColoringOfAGraph,
    CodingContractName.ShortestPathInAGrid,
    CodingContractName.SquareRoot,
  ];
  for (const [name, cct] of getRecordEntries(CodingContractTypes)) {
    if (skippedContractTypes.includes(name)) {
      continue;
    }
    test(name, () => {
      const state = cct.generate();
      const data = cct.getData ? cct.getData(state) : state;
      const answer = cct.getAnswer(data);
      expect(answer).not.toBeNull();
      expect(cct.solver(state, answer)).toStrictEqual(true);
    });
  }
});
