import { Arg, CellErrorType, DEFAULT_LOCALE, EvaluationError, Model } from "../../src";
import { arg } from "../../src/functions/arguments";
import { functionRegistry } from "../../src/functions/function_registry";
import { toScalar } from "../../src/functions/helper_matrices";
import { isEvaluationError, toBoolean, toNumber } from "../../src/functions/helpers";
import { toXC } from "../../src/helpers/coordinates";
import { setCellContent, setCellFormat, updateLocale } from "../test_helpers/commands_helpers";
import { FR_LOCALE } from "../test_helpers/constants";
import { getCellError, getEvaluatedCell } from "../test_helpers/getters_helpers";
import {
  addToRegistry,
  clearFunctions,
  createModelFromGrid,
  evaluateCell,
} from "../test_helpers/helpers";

describe("functions", () => {
  test("can add a function", () => {
    const val = evaluateCell("A1", { A1: "=DOUBLEDOUBLE(3)" });
    expect(val).toBe("#NAME?");
    addToRegistry(functionRegistry, "DOUBLEDOUBLE", {
      description: "Double the first argument",
      compute: function (arg) {
        return { value: 2 * toNumber(toScalar(arg), DEFAULT_LOCALE) };
      },
      args: [arg("number (number)", "my number")],
    });
    expect(evaluateCell("A1", { A1: "=DOUBLEDOUBLE(3)" })).toBe(6);
  });

  test("can not add a function with invalid name", () => {
    const createBadFunction = () => {
      addToRegistry(functionRegistry, "TEST*FUNCTION", {
        description: "Double the first argument",
        compute: () => ({ value: 0 }),
        args: [],
      });
    };
    expect(createBadFunction).toThrow(
      "Invalid function name TEST*FUNCTION. Function names can exclusively contain alphanumerical values separated by dots (.)"
    );
  });

  test("can add a function with underscore", () => {
    const createBadFunction = () => {
      addToRegistry(functionRegistry, "TEST_FUNCTION", {
        description: "Double the first argument",
        compute: () => ({ value: 0 }),
        args: [],
      });
    };
    expect(createBadFunction).not.toThrow();
  });

  test("Function can return value depending on input values", () => {
    const model = new Model();
    addToRegistry(functionRegistry, "RETURN.VALUE.DEPENDING.ON.INPUT.VALUE", {
      description: "return value depending on input value",
      compute: function (arg) {
        return { value: toNumber(toScalar(arg), DEFAULT_LOCALE) * 2 };
      },
      args: [arg("number (number)", "blabla")],
    });
    setCellContent(model, "A1", "21");
    setCellContent(model, "A2", "42");
    setCellContent(model, "B1", "=RETURN.VALUE.DEPENDING.ON.INPUT.VALUE(A1)");
    setCellContent(model, "B2", "=RETURN.VALUE.DEPENDING.ON.INPUT.VALUE(A2)");
    expect(getEvaluatedCell(model, "B1").value).toBe(42);
    expect(getEvaluatedCell(model, "B2").value).toBe(84);
  });

  test("Function can return value depending on input error", () => {
    const model = new Model();
    addToRegistry(functionRegistry, "RETURN.VALUE.DEPENDING.ON.INPUT.ERROR", {
      description: "return value depending on input error",
      compute: function (arg: Arg) {
        return { value: isEvaluationError(toScalar(arg)?.value) };
      },
      args: [arg("arg (any)", "blabla")],
    });
    setCellContent(model, "A1", "=SQRT(-1)");
    setCellContent(model, "B1", "=RETURN.VALUE.DEPENDING.ON.INPUT.ERROR(A1)");
    setCellContent(model, "B2", "=RETURN.VALUE.DEPENDING.ON.INPUT.ERROR(A2)");
    expect(getEvaluatedCell(model, "B1").value).toBe(true);
    expect(getEvaluatedCell(model, "B2").value).toBe(false);
  });

  test("Function can return error depending on input value", () => {
    const model = new Model();
    addToRegistry(functionRegistry, "RETURN.ERROR.DEPENDING.ON.INPUT.VALUE", {
      description: "return value depending on input error",
      compute: function (arg) {
        const error = new EvaluationError("Les calculs sont pas bons KEVIN !");
        return toBoolean(toScalar(arg)) ? error : { value: "ceci n'est pas une erreur" };
      },
      args: [arg("arg (any)", "blabla")],
    });
    setCellContent(model, "B1", "=RETURN.ERROR.DEPENDING.ON.INPUT.VALUE(true)");
    setCellContent(model, "B2", "=RETURN.ERROR.DEPENDING.ON.INPUT.VALUE(false)");
    expect(getEvaluatedCell(model, "B1")?.value).toBe("#ERROR");
    expect(getCellError(model, "B1")).toBe("Les calculs sont pas bons KEVIN !");
    expect(getEvaluatedCell(model, "B2").value).toBe("ceci n'est pas une erreur");
  });

  test("Function can return error depending on input error", () => {
    const model = new Model();
    addToRegistry(functionRegistry, "RETURN.ERROR.DEPENDING.ON.INPUT.ERROR", {
      description: "return value depending on input error",
      compute: function (arg) {
        return {
          value:
            toScalar(arg)?.value === CellErrorType.BadExpression
              ? CellErrorType.CircularDependency
              : CellErrorType.InvalidReference,
        };
      },
      args: [arg("arg (any)", "blabla")],
    });
    setCellContent(model, "A1", "=ThatDoesNotMeanAnything");
    setCellContent(model, "B1", "=RETURN.ERROR.DEPENDING.ON.INPUT.ERROR(A1)");
    setCellContent(model, "B2", "=RETURN.ERROR.DEPENDING.ON.INPUT.ERROR(SQRT(-1))");
    expect(getEvaluatedCell(model, "B1").value).toBe("#CYCLE");
    expect(getEvaluatedCell(model, "B2").value).toBe("#REF");
  });

  test("Function can return format depending on input format", () => {
    const model = new Model();
    addToRegistry(functionRegistry, "RETURN.FORMAT.DEPENDING.ON.INPUT.FORMAT", {
      description: "return format depending on input format",
      compute: function (arg) {
        return { value: 42, format: toScalar(arg)?.format };
      },
      args: [arg("number (number)", "blabla")],
    });
    setCellContent(model, "A1", "42");
    setCellFormat(model, "A1", "0%");
    setCellContent(model, "A2", "42");
    setCellFormat(model, "A2", "#,##0.00");
    setCellContent(model, "B1", "=RETURN.FORMAT.DEPENDING.ON.INPUT.FORMAT(A1)");
    setCellContent(model, "B2", "=RETURN.FORMAT.DEPENDING.ON.INPUT.FORMAT(A2)");
    expect(getEvaluatedCell(model, "B1").format).toBe("0%");
    expect(getEvaluatedCell(model, "B2").format).toBe("#,##0.00");
  });

  test("Function can return format depending on input value", () => {
    const model = new Model();
    addToRegistry(functionRegistry, "RETURN.FORMAT.DEPENDING.ON.INPUT.VALUE", {
      description: "return format depending on input value",
      compute: function (arg) {
        const value = toNumber(toScalar(arg), DEFAULT_LOCALE);
        return {
          value,
          format: value >= 0 ? "0%" : "#,##0.00",
        };
      },
      args: [arg("number (number)", "blabla")],
    });
    setCellContent(model, "A1", "42");
    setCellContent(model, "A2", "-42");
    setCellContent(model, "B1", "=RETURN.FORMAT.DEPENDING.ON.INPUT.VALUE(A1)");
    setCellContent(model, "B2", "=RETURN.FORMAT.DEPENDING.ON.INPUT.VALUE(A2)");
    expect(getEvaluatedCell(model, "B1").format).toBe("0%");
    expect(getEvaluatedCell(model, "B2").format).toBe("#,##0.00");
  });

  describe("Functions output can be used as a reference", () => {
    beforeEach(() => {
      clearFunctions();
      addToRegistry(functionRegistry, "GET.VALUE", {
        args: [arg("cell (any)", "blabla")],
        description: "Get the value of a cell",
        computeArray: function (arg) {
          return arg || { value: 0 };
        },
      });
      addToRegistry(functionRegistry, "GET.POSITION", {
        args: [arg("cell (any)", "blabla")],
        description: "Get the position of a cell if argument is a reference",
        compute: function (arg) {
          const _arg = toScalar(arg);
          const position = _arg?.position;
          if (position === undefined) {
            return { value: "nope" };
          }
          return { value: toXC(position.col, position.row) };
        },
      });
    });

    test("Cant use references with non-cell values", () => {
      const model = new Model();
      setCellContent(model, "A1", "42");
      setCellContent(model, "B1", "=GET.POSITION(42)");
      expect(getEvaluatedCell(model, "B1").value).toBe("nope");
      setCellContent(model, "B2", '=GET.POSITION("string")');
      expect(getEvaluatedCell(model, "B2").value).toBe("nope");
      setCellContent(model, "B3", "=GET.POSITION(TRUE)");
      expect(getEvaluatedCell(model, "B3").value).toBe("nope");
    });

    test("Can return correct reference for direct cell references", () => {
      const model = new Model();
      setCellContent(model, "A1", "10");
      setCellContent(model, "A2", "=A1");
      setCellContent(model, "A3", "=A2");
      setCellContent(model, "B1", "=GET.POSITION(A1)");
      setCellContent(model, "B2", "=GET.POSITION(A2)");
      setCellContent(model, "B3", "=GET.POSITION(A3)");
      expect(getEvaluatedCell(model, "B1").value).toBe("A1");
      expect(getEvaluatedCell(model, "B2").value).toBe("A2");
      expect(getEvaluatedCell(model, "B3").value).toBe("A3");
    });

    test("Can use function output as a reference", () => {
      const model = new Model();
      setCellContent(model, "A1", "5");
      setCellContent(model, "A2", "=A1");

      setCellContent(model, "B1", "=GET.VALUE(A2)");
      expect(getEvaluatedCell(model, "B1").value).toBe(5);
      setCellContent(model, "B2", "=GET.POSITION(GET.VALUE(A2))");
      expect(getEvaluatedCell(model, "B2").value).toBe("A2");

      setCellContent(model, "C1", "=GET.VALUE(42)");
      expect(getEvaluatedCell(model, "C1").value).toBe(42);
      setCellContent(model, "C2", "=GET.POSITION(GET.VALUE(42))");
      expect(getEvaluatedCell(model, "C2").value).toBe("nope");
    });
  });

  test("Can use a custom evaluation context in a function", () => {
    const model = new Model(
      {},
      {
        custom: {
          coucou: "Raoul",
        },
      }
    );
    addToRegistry(functionRegistry, "GETCOUCOU", {
      description: "Get coucou's name",
      compute: function () {
        return { value: (this as any).coucou };
      },
      args: [],
    });
    setCellContent(model, "A1", "=GETCOUCOU()");
    expect(getEvaluatedCell(model, "A1").value).toBe("Raoul");
  });

  test("Can use an evaluation getter in a function", () => {
    const model = new Model();
    addToRegistry(functionRegistry, "GETNUMBERCOLS", {
      description: "Get the number of columns",
      compute: function () {
        const sheetId = this.getters.getSheetIds()[0];
        return { value: this.getters.getNumberCols(sheetId) };
      },
      args: [],
    });
    expect(evaluateCell("A1", { A1: "=GETNUMBERCOLS()" })).toBe(
      model.getters.getNumberCols(model.getters.getActiveSheetId())
    );
  });
  test("Cannot use a UI getter in a function", () => {
    jest.spyOn(console, "error").mockImplementation(() => {});
    const model = new Model();
    addToRegistry(functionRegistry, "GETNUMBERCOLS", {
      description: "Get the number of columns",
      compute: function () {
        //@ts-ignore UI Getter should not be available
        const sheetId = this.getters.getActiveSheetId();
        return { value: this.getters.getNumberCols(sheetId) };
      },
      args: [],
    });
    setCellContent(model, "A1", "=GETNUMBERCOLS()");
    expect(getEvaluatedCell(model, "A1").value).toBe(CellErrorType.GenericError);
    expect(getCellError(model, "A1")).toContain("this.getters.getActiveSheetId is not a function");
  });

  test("undefined fallback to the zero value in a function", () => {
    addToRegistry(functionRegistry, "UNDEFINED", {
      description: "undefined",
      // @ts-expect-error can happen in a vanilla javascript code base
      compute: function () {
        return { value: undefined };
      },
      args: [],
    });
    expect(evaluateCell("A1", { A1: "=UNDEFINED()" })).toBe(0);
  });

  describe("check type of arguments", () => {
    test("reject non-range argument when expecting only range argument", () => {
      addToRegistry(functionRegistry, "RANGEEXPECTED", {
        description: "function expect number in 1st arg",
        compute: (arg) => {
          return { value: true };
        },
        args: [arg("arg1 (range<any>)", "1st argument")],
      });

      addToRegistry(functionRegistry, "FORMULA_RETURNING_RANGE", {
        description: "function returning range",
        computeArray: () => {
          return [[{ value: "cucumber" }]];
        },
        args: [],
      });

      addToRegistry(functionRegistry, "FORMULA_NOT_RETURNING_RANGE", {
        description: "function returning range",
        compute: () => {
          return { value: "cucumber" };
        },
        args: [],
      });

      addToRegistry(functionRegistry, "FORMULA_RETURNING_ERROR", {
        description: "function returning ERROR",
        compute: () => {
          return { value: "#ERROR" };
        },
        args: [],
      });

      addToRegistry(functionRegistry, "FORMULA_TROWING_ERROR", {
        description: "function trowing error",
        compute: () => {
          throw new EvaluationError("NOP");
        },
        args: [],
      });

      addToRegistry(functionRegistry, "FORMULA_RETURNING_RANGE_WITH_ERROR", {
        description: "function returning range",
        computeArray: () => {
          return [[{ value: "#ERROR" }]];
        },
        args: [],
      });

      addToRegistry(functionRegistry, "FORMULA_RETURNING_RANGE_TROWING_ERROR", {
        description: "function returning range",
        compute: () => {
          throw new EvaluationError("NOP");
        },
        args: [],
      });

      const m = new Model();
      const errorMessage =
        "Function RANGEEXPECTED expects the parameter '1' to be reference to a cell or range.";

      setCellContent(m, "A1", "=RANGEEXPECTED(42)");
      expect(getEvaluatedCell(m, "A1").value).toBe("#BAD_EXPR");
      expect(getCellError(m, "A1")).toBe(errorMessage);

      setCellContent(m, "B1", '=RANGEEXPECTED("test")');
      expect(getEvaluatedCell(m, "B1").value).toBe("#BAD_EXPR");
      expect(getCellError(m, "B1")).toBe(errorMessage);

      setCellContent(m, "C1", "=RANGEEXPECTED(TRUE)");
      expect(getEvaluatedCell(m, "C1").value).toBe("#BAD_EXPR");
      expect(getCellError(m, "C1")).toBe(errorMessage);

      setCellContent(m, "D1", "=RANGEEXPECTED(FORMULA_NOT_RETURNING_RANGE())");
      expect(getEvaluatedCell(m, "D1").value).toBe("#BAD_EXPR");
      expect(getCellError(m, "D1")).toBe(errorMessage);

      setCellContent(m, "E1", "=RANGEEXPECTED(A1)");
      expect(getEvaluatedCell(m, "E1").value).toBe(true);

      setCellContent(m, "F1", "=RANGEEXPECTED(A1:A1)");
      expect(getEvaluatedCell(m, "F1").value).toBe(true);

      setCellContent(m, "G1", "=RANGEEXPECTED(A1:A2)");
      expect(getEvaluatedCell(m, "G1").value).toBe(true);

      setCellContent(m, "H1", "=RANGEEXPECTED(A1:A$2)");
      expect(getEvaluatedCell(m, "H1").value).toBe(true);

      setCellContent(m, "I1", "=RANGEEXPECTED(sheet1!A1:A$2)");
      expect(getEvaluatedCell(m, "I1").value).toBe(true);

      setCellContent(m, "J1", "=RANGEEXPECTED(FORMULA_RETURNING_RANGE())");
      expect(getEvaluatedCell(m, "J1").value).toBe(true);

      setCellContent(m, "K1", "=RANGEEXPECTED(FORMULA_TROWING_ERROR())");
      expect(getEvaluatedCell(m, "K1").value).toBe("#BAD_EXPR");
      expect(getCellError(m, "K1")).toBe(errorMessage);

      setCellContent(m, "L1", "=RANGEEXPECTED(FORMULA_RETURNING_RANGE_WITH_ERROR())");
      expect(getEvaluatedCell(m, "L1").value).toBe(true);

      setCellContent(m, "M1", "=RANGEEXPECTED(FORMULA_RETURNING_RANGE_TROWING_ERROR())");
      expect(getEvaluatedCell(m, "M1").value).toBe("#BAD_EXPR");
      expect(getCellError(m, "M1")).toBe(errorMessage);
    });

    test("simple argument value from a single cell or range reference", () => {
      const m = new Model();

      addToRegistry(functionRegistry, "SIMPLE_VALUE_EXPECTED", {
        description: "does not accept a range",
        compute: (arg) => {
          return { value: true };
        },
        args: [{ name: "arg1", description: "", type: ["NUMBER"] }],
      });

      setCellContent(m, "B1", "=SIMPLE_VALUE_EXPECTED(A1)");
      expect(getEvaluatedCell(m, "B1").value).toBe(true);

      setCellContent(m, "B2", "=SIMPLE_VALUE_EXPECTED(A1:A2)");
      expect(getEvaluatedCell(m, "B2").value).toBe(true);
      expect(getEvaluatedCell(m, "B3").value).toBe(true);
    });
  });

  test("functions with 'visitMatchingRange' take Locale into account", () => {
    const model = createModelFromGrid({
      A1: "0.5",
      A2: "1.5",
      A3: "0.5",
      A4: '="1.5"',
      A5: '="1.5"',
      A6: '="1,5"',
      B1: "0.5",
      C1: "=COUNTIF(A1:A3, B1)",
      C2: "=COUNTIF(A1:A3, 0.5)",
      C3: "=COUNTIF(A1:A6, 1.5)",
    });
    expect(getEvaluatedCell(model, "C1").value).toBe(2);
    expect(getEvaluatedCell(model, "C2").value).toBe(2);
    expect(getEvaluatedCell(model, "C3").value).toBe(3);

    updateLocale(model, FR_LOCALE);
    expect(getEvaluatedCell(model, "C1").value).toBe(2);
    expect(getEvaluatedCell(model, "C2").value).toBe(2);
    expect(getEvaluatedCell(model, "C3").value).toBe(2);
  });
});
