import { Player } from "@player";
import fs from "node:fs";
import type { ScriptFilePath } from "../../../src/Paths/ScriptFilePath";
import { loadGame, saveObject } from "../../../src/SaveObject";
import * as db from "../../../src/db";
import * as FileUtils from "../../../src/utils/FileUtils";
import type { SaveData } from "../../../src/types";
import { calculateExp } from "../../../src/PersonObjects/formulas/skill";
import { GetAllServers, GetServer } from "../../../src/Server/AllServers";

async function loadGameFromSaveData(saveData: SaveData) {
  // Simulate loading the data in IndexedDB
  const mockedLoad = jest.spyOn(db, "load");
  // We must use structuredClone(saveData) instead of saveData; otherwise, the check of mockedDownload won't catch wrong
  // changes in evaluateVersionCompatibility (e.g., unexpectedly mutating saveData before passing it to
  // downloadContentAsFile).
  mockedLoad.mockReturnValue(Promise.resolve(structuredClone(saveData)));

  // Simulate saving the data in IndexedDB
  jest.spyOn(db, "save").mockImplementation(() => Promise.resolve());

  const mockedDownload = jest.spyOn(FileUtils, "downloadContentAsFile").mockImplementation(() => {});

  await loadGame(saveData);

  return mockedDownload;
}

describe("v3", () => {
  test("v2.8.1 to v3.0.0", async () => {
    const saveData = new Uint8Array(fs.readFileSync("test/jest/Migration/save-files/v2.8.1.gz"));
    const mockedDownload = await loadGameFromSaveData(saveData);

    // Check if auto-migration works
    expect(
      Player.getHomeComputer()
        .scripts.get("a.js" as ScriptFilePath)
        ?.code.includes("ns.ui.openTail()"),
    ).toStrictEqual(true);
    if (!Player.corporation) {
      throw new Error("The save file does not have corporation data");
    }
    expect(Object.keys(Player.corporation.upgrades).includes("DreamSense")).toStrictEqual(false);
    expect(Player.corporation.funds).toStrictEqual(110e9);

    // Check if evaluateVersionCompatibility correctly loads the data in IndexedDB and passes it to downloadContentAsFile
    expect(mockedDownload).toHaveBeenCalledWith(saveData, "bitburnerSave_backup_2.8.1_1756913326.json.gz");
  });

  test.each([
    ["test/jest/Migration/save-files/v2.8.1_500int.gz", 1773597870229, 1773597871370, undefined, 500, 300],
    ["test/jest/Migration/save-files/v2.8.1_500int_override_100int.gz", 1773597926723, 1773597928370, 100, 500, 300],
    ["test/jest/Migration/save-files/v2.8.1_500int_override_1000int.gz", 1773597951205, 1773597953370, 1000, 500, 300],
  ])("%s", async (path, lastSave, lastUpdate, intelligenceOverride, playerInt, sleeveInt) => {
    const saveData = new Uint8Array(fs.readFileSync(path));
    const mockedDownload = await loadGameFromSaveData(saveData);
    expect(Player.lastSave).toStrictEqual(lastSave);

    const playerIntExp =
      intelligenceOverride !== undefined && intelligenceOverride < playerInt
        ? calculateExp(intelligenceOverride, 1)
        : calculateExp(playerInt, 1);
    const sleeveIntExp =
      intelligenceOverride !== undefined && intelligenceOverride < sleeveInt
        ? calculateExp(intelligenceOverride, 1)
        : calculateExp(sleeveInt, 1);
    const persistentPlayerIntExp = calculateExp(playerInt, 1);
    const persistentSleeveIntExp = calculateExp(sleeveInt, 1);

    // Check if Player.exp.intelligence and Player.persistentIntelligenceData.exp are migrated.
    expect(Player.bitNodeOptions.intelligenceOverride).toStrictEqual(intelligenceOverride);
    expect(Player.exp.intelligence).toStrictEqual(playerIntExp);
    expect(Player.persistentIntelligenceData.exp).toStrictEqual(persistentPlayerIntExp);
    for (const sleeve of Player.sleeves) {
      expect(sleeve.exp.intelligence).toStrictEqual(sleeveIntExp);
      expect(sleeve.persistentIntelligenceData.exp).toStrictEqual(persistentSleeveIntExp);
    }

    const expGain = 1e9;
    // Gain exp and check if it is accumulated correctly.
    Player.gainIntelligenceExp(expGain);
    expect(Player.exp.intelligence).toStrictEqual(playerIntExp + expGain);
    expect(Player.persistentIntelligenceData.exp).toStrictEqual(persistentPlayerIntExp + expGain);
    for (const sleeve of Player.sleeves) {
      sleeve.gainIntelligenceExp(expGain);
      expect(sleeve.exp.intelligence).toStrictEqual(sleeveIntExp + expGain);
      expect(sleeve.persistentIntelligenceData.exp).toStrictEqual(persistentSleeveIntExp + expGain);
    }

    // Save and reload.
    await saveObject.saveGame();
    await loadGameFromSaveData(await saveObject.getSaveData());
    expect(Player.lastSave).not.toStrictEqual(lastSave);

    // Check if gained exp is saved correctly.
    expect(Player.bitNodeOptions.intelligenceOverride).toStrictEqual(intelligenceOverride);
    expect(Player.exp.intelligence).toStrictEqual(playerIntExp + expGain);
    expect(Player.persistentIntelligenceData.exp).toStrictEqual(persistentPlayerIntExp + expGain);
    for (const sleeve of Player.sleeves) {
      expect(sleeve.exp.intelligence).toStrictEqual(sleeveIntExp + expGain);
      expect(sleeve.persistentIntelligenceData.exp).toStrictEqual(persistentSleeveIntExp + expGain);
    }

    expect(mockedDownload).toHaveBeenCalledWith(
      saveData,
      `bitburnerSave_backup_2.8.1_${Math.round(lastUpdate / 1000)}.json.gz`,
    );
  });

  describe("Intelligence migration bug", () => {
    test("No change in exp and skill level", async () => {
      const saveData = new Uint8Array(fs.readFileSync("test/jest/Migration/save-files/v2.8.1_SF1.1_SF10.3.gz"));
      const mockedDownload = await loadGameFromSaveData(saveData);

      for (const person of [Player, ...Player.sleeves]) {
        expect(person.persistentIntelligenceData.exp).toStrictEqual(0);
        expect(person.exp.intelligence).toStrictEqual(0);
        expect(person.skills.intelligence).toStrictEqual(0);
      }

      expect(mockedDownload).toHaveBeenCalledWith(saveData, "bitburnerSave_backup_2.8.1_1776173824.json.gz");
    });
    test("Reset wrong exp and skill level", async () => {
      const saveData = new Uint8Array(fs.readFileSync("test/jest/Migration/save-files/v3.0.0_int_migration_bug.gz"));
      const mockedDownload = await loadGameFromSaveData(saveData);

      for (const person of [Player, ...Player.sleeves]) {
        expect(person.persistentIntelligenceData.exp).toStrictEqual(0);
        expect(person.exp.intelligence).toStrictEqual(0);
        expect(person.skills.intelligence).toStrictEqual(0);
      }

      expect(mockedDownload).not.toHaveBeenCalled();
    });
  });

  test("Malformed hostname", async () => {
    const saveData = new Uint8Array(fs.readFileSync("test/jest/Migration/save-files/malformed-hostname.gz"));
    await loadGameFromSaveData(saveData);
    for (const server of GetAllServers(true)) {
      expect(server.hostname.isWellFormed()).toBe(true);
      for (const script of server.scripts.values()) {
        expect(script.server).toStrictEqual(server.hostname);
      }
      if (server.savedScripts) {
        for (const script of server.savedScripts) {
          expect(script.server).toStrictEqual(server.hostname);
        }
      }
      for (const hostname of server.serversOnNetwork) {
        expect(hostname.isWellFormed()).toBe(true);
        expect(GetServer(hostname)).not.toBeNull();
      }
    }
    expect(() => Player.getCurrentServer()).not.toThrow();
  });
});
