import {
  processStringSync,
  encodeJSBinding,
  defaultJSSetup,
} from "../src/index"
import { UUID_REGEX } from "./constants"
import tk from "timekeeper"

const DATE = "2021-01-21T12:00:00"
tk.freeze(DATE)

const processJS = (js: string, context?: object): any => {
  return processStringSync(encodeJSBinding(js), context)
}

describe("Javascript", () => {
  beforeAll(() => {
    defaultJSSetup()
  })

  describe("Test the JavaScript helper", () => {
    it("should execute a simple expression", () => {
      const output = processJS(`return 1 + 2`)
      expect(output).toBe(3)
    })

    it("should be able to use primitive bindings", () => {
      const output = processJS(`return $("foo")`, {
        foo: "bar",
      })
      expect(output).toBe("bar")
    })

    it("should be able to use an object binding", () => {
      const output = processJS(`return $("foo").bar`, {
        foo: {
          bar: "baz",
        },
      })
      expect(output).toBe("baz")
    })

    it("should be able to use a complex object binding", () => {
      const output = processJS(`return $("foo").bar[0].baz`, {
        foo: {
          bar: [
            {
              baz: "shazbat",
            },
          ],
        },
      })
      expect(output).toBe("shazbat")
    })

    it("should be able to use a deep binding", () => {
      const output = processJS(`return $("foo.bar.baz")`, {
        foo: {
          bar: {
            baz: "shazbat",
          },
        },
      })
      expect(output).toBe("shazbat")
    })

    it("should be able to return an object", () => {
      const output = processJS(`return $("foo")`, {
        foo: {
          bar: {
            baz: "shazbat",
          },
        },
      })
      expect(output.bar.baz).toBe("shazbat")
    })

    it("should be able to return an array", () => {
      const output = processJS(`return $("foo")`, {
        foo: ["a", "b", "c"],
      })
      expect(output[2]).toBe("c")
    })

    it("should be able to return null", () => {
      const output = processJS(`return $("foo")`, {
        foo: null,
      })
      expect(output).toBe(null)
    })

    it("should be able to return undefined", () => {
      const output = processJS(`return $("foo")`, {
        foo: undefined,
      })
      expect(output).toBe(undefined)
    })

    it("should be able to return 0", () => {
      const output = processJS(`return $("foo")`, {
        foo: 0,
      })
      expect(output).toBe(0)
    })

    it("should be able to return an empty string", () => {
      const output = processJS(`return $("foo")`, {
        foo: "",
      })
      expect(output).toBe("")
    })

    it("should be able to use a deep array binding", () => {
      const output = processJS(`return $("foo.0.bar")`, {
        foo: [
          {
            bar: "baz",
          },
        ],
      })
      expect(output).toBe("baz")
    })

    it("should handle errors", () => {
      expect(processJS(`throw "Error"`)).toEqual("Error")
    })

    it("should timeout after one second", () => {
      const output = processJS(`while (true) {}`)
      expect(output).toBe("Timed out while executing JS")
    })

    it("should prevent access to the process global", async () => {
      expect(processJS(`return process`)).toEqual(
        "ReferenceError: process is not defined"
      )
    })
  })

  describe("check JS helpers", () => {
    it("should error if using the format helper. not helpers.", () => {
      expect(processJS(`return helper.toInt(4.3)`)).toEqual(
        "ReferenceError: helper is not defined"
      )
    })

    it("should be able to use toInt", () => {
      const output = processJS(`return helpers.toInt(4.3)`)
      expect(output).toBe(4)
    })

    it("should be able to use uuid", () => {
      const output = processJS(`return helpers.uuid()`)
      expect(output).toMatch(UUID_REGEX)
    })
  })

  describe("JS literal strings", () => {
    it("should be able to handle a literal string that is quoted (like role IDs)", () => {
      const output = processJS(`return $("'Custom'")`)
      expect(output).toBe("Custom")
    })
  })

  describe("mutability", () => {
    it("should not allow the context to be mutated", async () => {
      const context = { array: [1] }
      const result = await processJS(
        `
        const array = $("array");
        array.push(2);
        return array[1]
      `,
        context
      )
      expect(result).toEqual(2)
      expect(context.array).toEqual([1])
    })
  })

  describe("malice", () => {
    it("should not be able to call JS functions", () => {
      expect(processJS(`return alert("hello")`)).toEqual(
        "ReferenceError: alert is not defined"
      )

      expect(processJS(`return prompt("hello")`)).toEqual(
        "ReferenceError: prompt is not defined"
      )

      expect(processJS(`return confirm("hello")`)).toEqual(
        "ReferenceError: confirm is not defined"
      )

      expect(processJS(`return setTimeout(() => {}, 1000)`)).toEqual(
        "ReferenceError: setTimeout is not defined"
      )

      expect(processJS(`return setInterval(() => {}, 1000)`)).toEqual(
        "ReferenceError: setInterval is not defined"
      )
    })
  })

  // the test cases here were extracted from templates/real world examples of JS in Budibase
  describe("real test cases from Budicloud", () => {
    const context = {
      "Unit Value": 2,
      Quantity: 1,
    }
    it("handle test case 1", async () => {
      const result = await processJS(
        `
        var Gross = $("[Unit Value]") * $("[Quantity]")
        return Gross.toFixed(2)`,
        context
      )
      expect(result).toBeDefined()
      expect(result).toBe("2.00")
    })

    it("handle test case 2", async () => {
      const todayDate = new Date()
      // add a year and a month
      todayDate.setMonth(new Date().getMonth() + 1)
      todayDate.setFullYear(todayDate.getFullYear() + 1)
      const context = {
        "Purchase Date": DATE,
        today: todayDate.toISOString(),
      }
      const result = await processJS(
        `
        var purchase = new Date($("[Purchase Date]"));
        let purchaseyear = purchase.getFullYear();
        let purchasemonth = purchase.getMonth();

        var today = new Date($("today"));
        let todayyear = today.getFullYear();
        let todaymonth = today.getMonth();

        var age = todayyear - purchaseyear

        if (((todaymonth - purchasemonth) < 6) == true){
          return age
        }
        `,
        context
      )
      expect(result).toBeDefined()
      expect(result).toBe(1)
    })

    it("should handle test case 3", async () => {
      const context = {
        Escalate: true,
        "Budget ($)": 1100,
      }
      const result = await processJS(
        `
        if ($("[Escalate]") == true) {
          if ($("Budget ($)") <= 1000)
            {return 2;}
          if ($("Budget ($)") > 1000) 
            {return 3;}
          }
          else {
            if ($("Budget ($)") <= 1000)
              {return 1;}
            if ($("Budget ($)") > 1000)
              if ($("Budget ($)") < 10000) 
                {return 2;}
              else 
                {return 3}
          }
        `,
        context
      )
      expect(result).toBeDefined()
      expect(result).toBe(3)
    })

    it("should handle test case 4", async () => {
      const context = {
        "Time Sheets": ["a", "b"],
      }
      const result = await processJS(
        `
      let hours = 0
      if (($("[Time Sheets]") != null) == true){
        for (i = 0; i < $("[Time Sheets]").length; i++){
          let hoursLogged = "Time Sheets." + i + ".Hours"
          hours += $(hoursLogged)
        }
        return hours
      }
      if (($("[Time Sheets]") != null) == false){
        return hours
      }
      `,
        context
      )
      expect(result).toBeDefined()
      expect(result).toBe("0ab")
    })

    it("should handle test case 5", async () => {
      const context = {
        change: JSON.stringify({ a: 1, primaryDisplay: "a" }),
        previous: JSON.stringify({ a: 2, primaryDisplay: "b" }),
      }
      const result = await processJS(
        `
      let change = $("[change]") ? JSON.parse($("[change]")) : {}
      let previous = $("[previous]") ? JSON.parse($("[previous]")) : {}

      function simplifyLink(originalKey, value, parent) {
        if (Array.isArray(value)) {
          if (value.filter(item => Object.keys(item || {}).includes("primaryDisplay")).length > 0) {
            parent[originalKey] = value.map(link => link.primaryDisplay)
          }
        }
      }

      for (let entry of Object.entries(change)) {
        simplifyLink(entry[0], entry[1], change)
      }
      for (let entry of Object.entries(previous)) {
        simplifyLink(entry[0], entry[1], previous)
      }
      
      let diff = Object.fromEntries(Object.entries(change).filter(([k, v]) => previous[k]?.toString() !== v?.toString()))
      
      delete diff.audit_change
      delete diff.audit_previous
      delete diff._id
      delete diff._rev
      delete diff.tableId
      delete diff.audit
      
      for (let entry of Object.entries(diff)) {
        simplifyLink(entry[0], entry[1], diff)
      }
      
      return JSON.stringify(change)?.replaceAll(",\\"", ",\\n\\t\\"").replaceAll("{\\"", "{\\n\\t\\"").replaceAll("}", "\\n}")
      `,
        context
      )
      expect(result).toBe(`{\n\t"a":1,\n\t"primaryDisplay":"a"\n}`)
    })

    it("should handle test case 6", async () => {
      const context = {
        "Join Date": DATE,
      }
      const result = await processJS(
        `
        var rate = 5;
        var today = new Date();
        
        // comment
        function monthDiff(dateFrom, dateTo) {
         return dateTo.getMonth() - dateFrom.getMonth() + 
           (12 * (dateTo.getFullYear() - dateFrom.getFullYear()))
        }
        var serviceMonths = monthDiff( new Date($("[Join Date]")), today);
        var serviceYears = serviceMonths / 12;
        
        if (serviceYears >= 1 && serviceYears < 5){
          rate = 10;
        }
        if (serviceYears >= 5 && serviceYears < 10){
          rate = 15;
        }
        if (serviceYears >= 10){
          rate = 15;
          rate += 0.5 * (Number(serviceYears.toFixed(0)) - 10);
        }
        return rate;
        `,
        context
      )
      expect(result).toBe(10)
    })

    it("should handle test case 7", async () => {
      const context = {
        "P I": "Pass",
        "PA I": "Pass",
        "F I": "Fail",
        "V I": "Pass",
      }
      const result = await processJS(
        `if (($("[P I]") == "Pass") == true)
              if (($("[  P I]") == "Pass") == true)
                if (($("[F I]") == "Pass") == true)
                  if (($("[V I]") == "Pass") == true)
                    {return "Pass"}
            
            if (($("[PA I]") == "Fail") == true)
              {return "Fail"}
            if (($("[  P I]") == "Fail") == true)
              {return "Fail"}
            if (($("[F I]") == "Fail") == true)
              {return "Fail"}
            if (($("[V I]") == "Fail") == true)
              {return "Fail"}
            
            else
              {return ""}`,
        context
      )
      expect(result).toBe("Fail")
    })

    it("should handle test case 8", async () => {
      const context = {
        "T L": [{ Hours: 10 }],
        "B H": 50,
      }
      const result = await processJS(
        `var totalHours = 0;
            if (($("[T L]") != null) == true){
            for (let i = 0; i < ($("[T L]").length); i++){
              var individualHours = "T L." + i + ".Hours";
              var hoursNum = Number($(individualHours));
              totalHours += hoursNum;
            }
            return totalHours.toFixed(2);
            }
            if (($("[T L]") != null) == false) {
              return totalHours.toFixed(2);
            }
        `,
        context
      )
      expect(result).toBe("10.00")
    })

    it("should handle test case 9", async () => {
      const context = {
        "T L": [{ Hours: 10 }],
        "B H": 50,
      }
      const result = await processJS(
        `var totalHours = 0;
            if (($("[T L]") != null) == true){
            for (let i = 0; i < ($("[T L]").length); i++){
              var individualHours = "T L." + i + ".Hours";
              var hoursNum = Number($(individualHours));
              totalHours += hoursNum;
            }
            return ($("[B H]") - totalHours).toFixed(2);
            }
            if (($("[T L]") != null) == false) {
              return ($("[B H]") - totalHours).toFixed(2);
            }`,
        context
      )
      expect(result).toBe("40.00")
    })

    it("should handle test case 10", async () => {
      const context = {
        "F F": [{ "F S": 10 }],
      }
      const result = await processJS(
        `var rating = 0;
          
          if ($("[F F]") != null){
          for (i = 0; i < $("[F F]").length; i++){
            var individualRating = $("F F." + i + ".F S");
            rating += individualRating;
            }
            rating = (rating / $("[F F]").length);
          }
          return rating;
          `,
        context
      )
      expect(result).toBe(10)
    })
  })
})