import { Player } from "@player";
import { AugmentationName, GoColor, GoOpponent, GoPlayType } from "@enums";
import { Go } from "../../../src/Go/Go";
import {
  boardStateFromSimpleBoard,
  simpleBoardFromBoard,
  updatedBoardFromSimpleBoard,
} from "../../../src/Go/boardAnalysis/boardAnalysis";
import { resetAI } from "../../../src/Go/boardAnalysis/goAI";
import {
  cheatPlayTwoMoves,
  cheatRemoveRouter,
  cheatRepairOfflineNode,
  cheatSuccessChance,
  getChains,
  getControlledEmptyNodes,
  getGameState,
  getLiberties,
  getValidMoves,
  handlePassTurn,
  makePlayerMove,
  resetBoardState,
  validateMove,
} from "../../../src/Go/effects/netscriptGoImplementation";
import { getNewBoardState, getNewBoardStateFromSimpleBoard } from "../../../src/Go/boardState/boardState";
import { installAugmentations } from "../../../src/Augmentation/AugmentationHelpers";
import { getMockedNetscriptContext, initGameEnvironment, setupBasicTestingEnvironment } from "../Utilities";
import { NetscriptGo } from "../../../src/NetscriptFunctions/Go";

initGameEnvironment();

beforeEach(() => {
  setupBasicTestingEnvironment();
});

const mockLogger: (s: string) => void = jest.fn();
const mockCtx = getMockedNetscriptContext((_: string, txt: () => string) => {
  mockLogger(txt());
});

describe("Netscript Go API unit tests", () => {
  describe("makeMove() tests", () => {
    it("should handle invalid moves", () => {
      const board = ["XOO..", ".....", ".....", ".....", "....."];
      Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
      resetAI();

      expect(() => makePlayerMove(mockCtx, 0, 0)).toThrow(
        "Invalid move: 0 0. That node is already occupied by a piece.",
      );
    });

    it("should update the board with valid player moves", async () => {
      const board = ["OXX..", ".....", ".....", ".....", "....."];
      const boardState = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
      Go.currentGame = boardState;
      resetAI();

      await makePlayerMove(mockCtx, 1, 0);

      expect(mockLogger).toHaveBeenCalledWith("Go move played: 1, 0");
      expect(boardState.board[1]?.[0]?.color).toEqual(GoColor.black);
      expect(boardState.board[0]?.[0]?.color).toEqual(GoColor.empty);
    });
  });
  describe("passTurn() tests", () => {
    it("should handle pass attempts", async () => {
      Go.currentGame = getNewBoardState(7);
      resetAI();

      const result = await handlePassTurn(mockCtx);

      expect(result.type).toEqual(GoPlayType.move);
    });
  });

  describe("getBoardState() tests", () => {
    it("should correctly return a string version of the board state", () => {
      const board = ["OXX..", ".....", ".....", ".....", "..###"];
      const boardState = boardStateFromSimpleBoard(board);

      const result = simpleBoardFromBoard(boardState.board);

      expect(result).toEqual(board);
    });
  });

  describe("getGameState() tests", () => {
    it("should correctly retrieve the current game state", () => {
      const board = ["OXX..", ".....", "..#..", "...XX", "...X."];
      const boardState = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.black);
      boardState.previousBoards = ["OX.........#.....XX...X."];
      Go.currentGame = boardState;
      resetAI();

      const result = getGameState();

      expect(result).toEqual({
        currentPlayer: GoColor.white,
        whiteScore: 6.5,
        blackScore: 6,
        previousMove: [0, 2],
        bonusCycles: 0,
        komi: 5.5,
      });
    });
  });

  describe("resetBoardState() tests", () => {
    it("should set the player's board to the requested size and opponent", () => {
      const board = ["OXX..", ".....", ".....", ".....", "..###"];
      Go.currentGame = boardStateFromSimpleBoard(board);
      resetAI();

      const newBoard = resetBoardState(mockCtx, GoOpponent.SlumSnakes, 9);

      expect(newBoard?.[0].length).toEqual(9);
      expect(Go.currentGame.board.length).toEqual(9);
      expect(Go.currentGame.ai).toEqual(GoOpponent.SlumSnakes);
      expect(mockLogger).toHaveBeenCalledWith(`New game started: ${GoOpponent.SlumSnakes}, 9x9`);
    });
    it("should throw an error if an invalid opponent is requested", () => {
      const board = ["OXX..", ".....", ".....", ".....", "..###"];
      Go.currentGame = boardStateFromSimpleBoard(board);
      resetAI();

      expect(() => resetBoardState(mockCtx, GoOpponent.w0r1d_d43m0n, 9)).toThrow(
        `Invalid opponent requested (${GoOpponent.w0r1d_d43m0n}), this opponent has not yet been discovered`,
      );
    });
    it("should throw an error if an invalid size is requested", () => {
      const board = ["OXX..", ".....", ".....", ".....", "..###"];
      Go.currentGame = boardStateFromSimpleBoard(board);
      resetAI();

      expect(() => resetBoardState(mockCtx, GoOpponent.TheBlackHand, 31337)).toThrow(
        "Invalid subnet size requested (31337), size must be 5, 7, 9, or 13",
      );
    });
  });

  describe("getValidMoves() unit tests", () => {
    it("should return all valid and invalid moves on the board", () => {
      const board = ["XXO.#", "XO.O.", ".OOOO", "XXXXX", "X.X.X"];
      Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
      resetAI();

      const result = getValidMoves();

      expect(result).toEqual([
        [false, false, false, false, false],
        [false, false, false, false, false],
        [true, false, false, false, false],
        [false, false, false, false, false],
        [false, true, false, true, false],
      ]);
    });

    it("should correctly find available moves for a given board when playing as white", () => {
      const boardState = ["XOX..", "X.X.X", ".X..X", "...XX", "..XOO"];
      const mockNetscriptContext = getMockedNetscriptContext();

      const result = NetscriptGo().analysis.getValidMoves(mockNetscriptContext)(boardState, null, true);

      expect(result).toEqual([
        [false, false, false, true, true],
        [false, false, false, true, false],
        [true, false, true, true, false],
        [true, true, true, false, false],
        [true, true, false, false, false],
      ]);
    });

    it("should correctly find available moves for a given board when playing as white and given a prior board", () => {
      const boardState = ["#..##", ".....", "...O.", ".....", "....."];
      const mockNetscriptContext = getMockedNetscriptContext();

      const result = NetscriptGo().analysis.getValidMoves(mockNetscriptContext)(boardState, boardState, true);

      expect(result).toEqual([
        [false, true, true, false, false],
        [true, true, true, true, true],
        [true, true, true, false, true],
        [true, true, true, true, true],
        [true, true, true, true, true],
      ]);
    });

    it("should return all valid and invalid moves on the board, if a board is provided", () => {
      const currentBoard = [".....", ".....", ".....", ".....", "....."];
      Go.currentGame = boardStateFromSimpleBoard(currentBoard, GoOpponent.Daedalus, GoColor.white);
      resetAI();

      const board = getNewBoardStateFromSimpleBoard(
        ["..O.#", ".O.O.", ".OOOO", "XXXXX", "X.X.X"],
        undefined,
        GoOpponent.Netburners,
        GoColor.white,
      );
      const result = getValidMoves(board);

      expect(result).toEqual([
        [true, true, false, false, false],
        [true, false, false, false, false],
        [true, false, false, false, false],
        [false, false, false, false, false],
        [false, true, false, true, false],
      ]);
    });
  });

  describe("getChains() unit tests", () => {
    it("should assign an ID to all contiguous chains on the board", () => {
      const board = ["XXO.#", "XO.O.", ".OOOO", "XXXXX", "X.X.X"];
      Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
      resetAI();

      const result = getChains();

      expect(result[4][0]).toEqual(result[3][4]);
      expect(result[2][1]).toEqual(result[1][3]);
      expect(result[0][0]).toEqual(result[1][0]);
      expect(result[0][4]).toEqual(null);
    });
  });

  describe("getLiberties() unit tests", () => {
    it("should display the number of connected empty nodes for each chain on the board", () => {
      const board = ["XXO.#", "XO.O.", ".OOOO", "XXXXX", "X.X.X"];
      Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
      resetAI();

      const result = getLiberties();

      expect(result).toEqual([
        [1, 1, 2, -1, -1],
        [1, 4, -1, 4, -1],
        [-1, 4, 4, 4, 4],
        [3, 3, 3, 3, 3],
        [3, -1, 3, -1, 3],
      ]);
    });

    it("should show zero liberties for groups that would be captured and -1 for empty spaces or offline nodes", () => {
      const boardState = [".XXX#", "XOOOX", "XOXOX", "XOOOX", "XXXX."];
      const mockNetscriptContext = getMockedNetscriptContext();

      const result = NetscriptGo().analysis.getLiberties(mockNetscriptContext)(boardState);

      expect(result).toEqual([
        [-1, 1, 1, 1, -1],
        [2, 0, 0, 0, 1],
        [2, 0, 0, 0, 1],
        [2, 0, 0, 0, 1],
        [2, 2, 2, 2, -1],
      ]);
    });
  });
  describe("getControlledEmptyNodes() unit tests", () => {
    it("should show the owner of each empty node, if a single player has fully encircled it", () => {
      const board = ["XXO.#", "XO.O.", ".OOOO", "XXXXX", "X.X.X"];
      Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
      resetAI();

      const result = getControlledEmptyNodes();

      expect(result).toEqual(["...O#", "..O.O", "?....", ".....", ".X.X."]);
    });

    it("should show the details for the given board, if provided", () => {
      const currentBoard = [".....", ".....", ".....", ".....", "....."];
      Go.currentGame = boardStateFromSimpleBoard(currentBoard, GoOpponent.Daedalus, GoColor.white);
      resetAI();

      const board = updatedBoardFromSimpleBoard(["XXO.#", "XO.O.", ".OOOO", "XXXXX", "X.X.X"]);
      const result = getControlledEmptyNodes(board);

      expect(result).toEqual(["...O#", "..O.O", "?....", ".....", ".X.X."]);
    });
  });
  describe("setTestingBoardState() tests", () => {
    it("should set the board to the requested state", () => {
      const board = ["OXX..", ".....", ".....", ".....", "....."];
      Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
      resetAI();

      NetscriptGo().analysis.setTestingBoardState(mockCtx)(["XOX..", "X.X.X", ".X..X", "...XX", "..XOO"], null, true);

      const newBoard = simpleBoardFromBoard(Go.currentGame.board);
      expect(newBoard).toEqual(["XOX..", "X.X.X", ".X..X", "...XX", "..X.."]);
      expect(Go.currentGame.previousPlayer).toEqual(GoColor.black);
      expect(Go.currentGame.komiOverride).toEqual(5.5);
      expect(Go.currentGame.ai).toEqual(GoOpponent.none);
    });

    it("should set the board to the requested state, and set the last played color correctly", () => {
      const board = ["OXX..", ".....", ".....", ".....", "....."];
      Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
      resetAI();

      NetscriptGo().analysis.setTestingBoardState(mockCtx)(["XOX..", "X.X.X", ".X..X", "...XX", "..XOO"], 13);

      const newBoard = simpleBoardFromBoard(Go.currentGame.board);
      expect(newBoard).toEqual(["XOX..", "X.X.X", ".X..X", "...XX", "..X.."]);
      expect(Go.currentGame.previousPlayer).toEqual(GoColor.white);
      expect(Go.currentGame.komiOverride).toEqual(13);
      expect(Go.currentGame.ai).toEqual(GoOpponent.none);
    });
  });
  describe("cheatPlayTwoMoves() tests", () => {
    it("should handle invalid moves", () => {
      const board = ["XOO..", ".....", ".....", ".....", "....."];
      Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
      resetAI();
      expect(() =>
        validateMove(mockCtx, 0, 0, "playTwoMoves", {
          repeat: false,
          suicide: false,
        }),
      ).toThrow("The point 0,0 is occupied by a router, so you cannot place a router there");
    });

    it("should update the board with both player moves if nodes are unoccupied and cheat is successful", async () => {
      const board = ["OXX..", ".....", ".....", ".....", "....O"];
      Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
      resetAI();

      await cheatPlayTwoMoves(mockCtx, 4, 3, 3, 4, 0, 0);
      expect(mockLogger).toHaveBeenCalledWith("Cheat successful. Two go moves played: 4,3 and 3,4");
      expect(Go.currentGame.board[4]?.[3]?.color).toEqual(GoColor.black);
      expect(Go.currentGame.board[3]?.[4]?.color).toEqual(GoColor.black);
      expect(Go.currentGame.board[4]?.[4]?.color).toEqual(GoColor.empty);
    });

    it("should pass player turn to AI if the cheat is unsuccessful but player is not ejected", async () => {
      const board = ["OXX..", ".....", ".....", ".....", "....O"];
      Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
      resetAI();

      await cheatPlayTwoMoves(mockCtx, 4, 3, 3, 4, 2, 1);
      expect(mockLogger).toHaveBeenCalledWith("Cheat failed. Your turn has been skipped.");
      expect(Go.currentGame.board[4]?.[3]?.color).toEqual(GoColor.empty);
      expect(Go.currentGame.board[3]?.[4]?.color).toEqual(GoColor.empty);
      expect(Go.currentGame.board[4]?.[4]?.color).toEqual(GoColor.white);
    });

    it("should reset the board if the cheat is unsuccessful and the player is ejected", async () => {
      const board = ["OXX..", ".....", ".....", ".....", "....O"];
      Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
      resetAI();
      Go.currentGame.cheatCount = 1;

      await cheatPlayTwoMoves(mockCtx, 4, 3, 3, 4, 1, 0);
      expect(mockLogger).toHaveBeenCalledWith("Cheat failed! You have been ejected from the subnet.");
      expect(Go.currentGame.previousBoards).toEqual([]);
    });
  });
  describe("cheatRemoveRouter() tests", () => {
    it("should handle invalid moves", () => {
      const board = ["XOO..", ".....", ".....", ".....", "....."];
      Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
      resetAI();
      expect(() =>
        validateMove(mockCtx, 1, 0, "removeRouter", {
          emptyNode: false,
          requireNonEmptyNode: true,
          repeat: false,
          suicide: false,
        }),
      ).toThrow("The point 1,0 does not have a router on it, so you cannot clear this point with removeRouter().");
    });

    it("should remove the router if the move is valid", async () => {
      const board = ["XOO..", ".....", ".....", ".....", "....."];
      Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
      resetAI();

      await cheatRemoveRouter(mockCtx, 0, 0, 0, 0);

      expect(mockLogger).toHaveBeenCalledWith("Cheat successful. The point 0,0 was cleared.");
      expect(Go.currentGame.board[0][0]?.color).toEqual(GoColor.empty);
    });

    it("should reset the board if the cheat is unsuccessful and the player is ejected", async () => {
      const board = ["OXX..", ".....", ".....", ".....", "....O"];
      Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
      resetAI();
      Go.currentGame.cheatCount = 1;

      await cheatRemoveRouter(mockCtx, 0, 0, 1, 0);
      expect(mockLogger).toHaveBeenCalledWith("Cheat failed! You have been ejected from the subnet.");
      expect(Go.currentGame.previousBoards).toEqual([]);
    });
  });
  describe("cheatRepairOfflineNode() tests", () => {
    it("should handle invalid moves", () => {
      const board = ["XOO..", ".....", ".....", ".....", "....#"];
      Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
      resetAI();
      expect(() =>
        validateMove(mockCtx, 0, 0, "repairOfflineNode", {
          emptyNode: false,
          repeat: false,
          onlineNode: false,
          requireOfflineNode: true,
          suicide: false,
        }),
      ).toThrow("The node 0,0 is not offline, so you cannot repair the node.");
    });

    it("should update the board with the repaired node if the cheat is successful", async () => {
      const board = ["OXX..", ".....", ".....", ".....", "....#"];
      Go.currentGame = boardStateFromSimpleBoard(board, GoOpponent.Daedalus, GoColor.white);
      resetAI();

      await cheatRepairOfflineNode(mockCtx, 4, 4, 0, 0);
      expect(mockLogger).toHaveBeenCalledWith("Cheat successful. The point 4,4 was repaired.");
      expect(Go.currentGame.board[4]?.[4]?.color).toEqual(GoColor.empty);
    });
  });

  describe("Cheat success chance unit tests", () => {
    it("should have a base chance", () => {
      expect(cheatSuccessChance(0)).toEqual(0.6);
    });

    it("should have a scaled chance based on cheat count", () => {
      expect(cheatSuccessChance(4)).toEqual(0.6 * (0.7 - 0.08) ** 4);
    });

    it("should have a scaled chance based on layer cheat success level", () => {
      Player.setBitNodeNumber(13);
      Player.queueAugmentation(AugmentationName.BrachiBlades);
      Player.queueAugmentation(AugmentationName.GrapheneBrachiBlades);
      Player.queueAugmentation(AugmentationName.INFRARet);
      Player.queueAugmentation(AugmentationName.PCMatrix);
      Player.queueAugmentation(AugmentationName.NeuroFluxGovernor);
      installAugmentations();

      expect(cheatSuccessChance(4)).toEqual(0.6 * (0.7 - 0.08) ** 4 * Player.mults.crime_success);
    });
  });
});
