import { faker } from "@faker-js/faker"
import {
  generateMakeRequest,
  MakeRequestResponse,
} from "../api/routes/public/tests/utils"

import * as setup from "../api/routes/tests/utilities"
import { Datasource, FieldType, SourceName, Table } from "@budibase/types"
import _ from "lodash"

const config = setup.getConfig()

jest.unmock("node-fetch")

describe("row api - postgres", () => {
  let apiKey,
    makeRequest: MakeRequestResponse,
    postgresDatasource: Datasource,
    postgresTable: Table

  beforeEach(async () => {
    await config.init()
    apiKey = await config.generateApiKey()
    postgresDatasource = await config.createDatasource({
      type: "datasource",
      source: SourceName.POSTGRES,
      plus: true,
      config: {
        host: "192.168.1.98",
        port: 54321,
        database: "postgres",
        user: "root",
        password: "root",
        schema: "public",
        ssl: false,
        rejectUnauthorized: false,
        ca: false,
      },
    })

    makeRequest = generateMakeRequest(apiKey)

    postgresTable = await config.createTable({
      name: faker.lorem.word(),
      schema: {
        name: {
          name: "name",
          type: FieldType.STRING,
          constraints: {
            presence: true,
          },
        },
        description: {
          name: "description",
          type: FieldType.STRING,
        },
        value: {
          name: "value",
          type: FieldType.NUMBER,
        },
      },
      sourceId: postgresDatasource._id,
    })
  })

  afterAll(async () => {
    await config.end()
  })

  function createRandomRow() {
    return {
      name: faker.name.fullName(),
      description: faker.lorem.paragraphs(),
      value: +faker.random.numeric(),
    }
  }

  function createRow(
    row: {
      name: string
      description: string
      value: number
    },
    tableId?: string
  ) {
    return config.createRow({
      tableId: tableId || postgresTable._id,
      ...row,
    })
  }

  async function populateRows(count: number, tableId?: string) {
    return await Promise.all(
      Array(count)
        .fill({})
        .map(async () => {
          const rowData = createRandomRow()
          return {
            rowData,
            row: await createRow(rowData, tableId || postgresTable._id),
          }
        })
    )
  }

  describe("create a row", () => {
    test("Given than no row exists, adding a new row persists it", async () => {
      const newRow = createRandomRow()

      const res = await makeRequest(
        "post",
        `/tables/${postgresTable._id}/rows`,
        newRow
      )

      expect(res.status).toBe(200)

      const persistedRows = await config.getRows(postgresTable._id!)
      expect(persistedRows).toHaveLength(1)
      expect(persistedRows).toEqual([
        expect.objectContaining({
          ...res.body.data,
          ...newRow,
        }),
      ])
    })

    test("Given than no row exists, multiple rows can be persisted", async () => {
      const numberOfRows = 10
      const newRows = Array(numberOfRows).fill(createRandomRow())

      for (const newRow of newRows) {
        const res = await makeRequest(
          "post",
          `/tables/${postgresTable._id}/rows`,
          newRow
        )
        expect(res.status).toBe(200)
      }

      const persistedRows = await config.getRows(postgresTable._id!)
      expect(persistedRows).toHaveLength(numberOfRows)
      expect(persistedRows).toEqual(
        expect.arrayContaining(newRows.map(expect.objectContaining))
      )
    })
  })

  describe("update a row", () => {
    test("Given than a row exists, updating it persists it", async () => {
      let { rowData, row } = _.sample(await populateRows(10))!

      const newName = faker.random.words(3)
      const newValue = +faker.random.numeric()
      const updateRow = {
        name: newName,
        value: newValue,
      }

      const res = await makeRequest(
        "put",
        `/tables/${postgresTable._id}/rows/${row._id}`,
        updateRow
      )

      expect(res.status).toBe(200)

      const persistedRow = await config.getRow(postgresTable._id!, row._id!)

      expect(persistedRow).toEqual(
        expect.objectContaining({
          ...res.body.data,
          ...rowData,
          ...updateRow,
        })
      )
    })
  })

  describe("delete a row", () => {
    test("Given than a row exists, delete request removes it", async () => {
      const numberOfInitialRows = 5
      let { row } = _.sample(await populateRows(numberOfInitialRows))!

      const res = await makeRequest(
        "delete",
        `/tables/${postgresTable._id}/rows/${row._id}`
      )

      expect(res.status).toBe(200)

      const persistedRows = await config.getRows(postgresTable._id!)
      expect(persistedRows).toHaveLength(numberOfInitialRows - 1)

      expect(row._id).toBeDefined()
      expect(persistedRows).not.toContain(
        expect.objectContaining({ _id: row._id })
      )
    })
  })

  describe("retrieve a row", () => {
    test("Given than a table have a single row, the row can be retrieved successfully", async () => {
      const [{ rowData, row }] = await populateRows(1)

      const res = await makeRequest(
        "get",
        `/tables/${postgresTable._id}/rows/${row._id}`
      )

      expect(res.status).toBe(200)

      expect(res.body.data).toEqual(expect.objectContaining(rowData))
    })

    test("Given than a table have a multiple rows, a single row can be retrieved successfully", async () => {
      const rows = await populateRows(10)
      const { rowData, row } = _.sample(rows)!

      const res = await makeRequest(
        "get",
        `/tables/${postgresTable._id}/rows/${row._id}`
      )

      expect(res.status).toBe(200)

      expect(res.body.data).toEqual(expect.objectContaining(rowData))
    })
  })

  describe("search for rows", () => {
    const search = (tableId: string | undefined, body?: object) =>
      makeRequest("post", `/tables/${tableId}/rows/search`, body)

    describe("empty search", () => {
      test("Given than a table has no rows, search without query returns empty", async () => {
        const res = await search(postgresTable._id)

        expect(res.status).toBe(200)

        expect(res.body.data).toHaveLength(0)
      })

      test("Given than a table has multiple rows, search without query returns all of them", async () => {
        const rowsCount = 6
        const rows = await populateRows(rowsCount)

        const res = await search(postgresTable._id)

        expect(res.status).toBe(200)

        expect(res.body.data).toHaveLength(rowsCount)
        expect(res.body.data).toEqual(
          expect.arrayContaining(
            rows.map(r => expect.objectContaining(r.rowData))
          )
        )
      })

      test("Given than multiple tables have multiple rows, search only return the requested ones", async () => {
        await populateRows(2, (await config.createTable())._id)
        const rowsCount = 6
        await populateRows(rowsCount)
        await populateRows(2, (await config.createTable())._id)

        const res = await search(postgresTable._id)

        expect(res.status).toBe(200)

        expect(res.body.data).toHaveLength(rowsCount)
      })
    })

    test("Querying by a string field returns the rows with field containing or starting by that value", async () => {
      const name = faker.name.fullName()
      const rowsToFilter = [
        ...Array(2).fill({
          name,
          description: faker.lorem.paragraphs(),
          value: +faker.random.numeric(),
        }),
        ...Array(2).fill({
          name: `${name}${faker.random.alphaNumeric(5)}`,
          description: faker.lorem.paragraphs(),
          value: +faker.random.numeric(),
        }),
      ]

      await populateRows(3)
      for (const row of rowsToFilter) {
        await createRow(row, postgresTable._id)
      }
      await populateRows(1)

      const res = await search(postgresTable._id, {
        query: {
          string: {
            name,
          },
        },
      })

      expect(res.status).toBe(200)

      expect(res.body.data).toHaveLength(4)
      expect(res.body.data).toEqual(
        expect.arrayContaining(rowsToFilter.map(expect.objectContaining))
      )
    })

    test("Querying respects the limit fields", async () => {
      await populateRows(6)

      const res = await search(postgresTable._id, {
        limit: 2,
      })

      expect(res.status).toBe(200)

      expect(res.body.data).toHaveLength(2)
    })

    describe("sort", () => {
      beforeEach(async () => {
        const defaultValue = createRandomRow()

        await createRow(
          {
            ...defaultValue,
            name: "d",
            value: 3,
          },
          postgresTable._id
        )
        await createRow(
          { ...defaultValue, name: "aaa", value: 40 },
          postgresTable._id
        )
        await createRow(
          { ...defaultValue, name: "ccccc", value: -5 },
          postgresTable._id
        )
        await createRow(
          { ...defaultValue, name: "bb", value: 0 },
          postgresTable._id
        )
      })

      test("Querying respects the sort order when sorting ascending by a string value", async () => {
        const res = await search(postgresTable._id, {
          sort: {
            order: "ascending",
            column: "name",
            type: "string",
          },
        })

        expect(res.status).toBe(200)
        expect(res.body.data).toEqual([
          expect.objectContaining({ name: "aaa" }),
          expect.objectContaining({ name: "bb" }),
          expect.objectContaining({ name: "ccccc" }),
          expect.objectContaining({ name: "d" }),
        ])
      })

      test("Querying respects the sort order when sorting descending by a string value", async () => {
        const res = await search(postgresTable._id, {
          sort: {
            order: "descending",
            column: "name",
            type: "string",
          },
        })

        expect(res.status).toBe(200)
        expect(res.body.data).toEqual([
          expect.objectContaining({ name: "d" }),
          expect.objectContaining({ name: "ccccc" }),
          expect.objectContaining({ name: "bb" }),
          expect.objectContaining({ name: "aaa" }),
        ])
      })

      test("Querying respects the sort order when sorting ascending by a numeric value", async () => {
        const res = await search(postgresTable._id, {
          sort: {
            order: "ascending",
            column: "value",
            type: "number",
          },
        })

        expect(res.status).toBe(200)
        expect(res.body.data).toEqual([
          expect.objectContaining({ value: -5 }),
          expect.objectContaining({ value: 0 }),
          expect.objectContaining({ value: 3 }),
          expect.objectContaining({ value: 40 }),
        ])
      })

      test("Querying respects the sort order when sorting descending by a numeric value", async () => {
        const res = await search(postgresTable._id, {
          sort: {
            order: "descending",
            column: "value",
            type: "number",
          },
        })

        expect(res.status).toBe(200)
        expect(res.body.data).toEqual([
          expect.objectContaining({ value: 40 }),
          expect.objectContaining({ value: 3 }),
          expect.objectContaining({ value: 0 }),
          expect.objectContaining({ value: -5 }),
        ])
      })
    })
  })

  describe("get all rows", () => {
    const getAll = (tableId: string | undefined) =>
      makeRequest("get", `/tables/${postgresTable._id}/rows`)

    test("Given than a table has no rows, get returns empty", async () => {
      const res = await getAll(postgresTable._id)

      expect(res.status).toBe(200)

      expect(res.body.data).toHaveLength(0)
    })

    test("Given than a table has multiple rows, get returns all of them", async () => {
      const rowsCount = 6
      const rows = await populateRows(rowsCount)

      const res = await getAll(postgresTable._id)

      expect(res.status).toBe(200)

      expect(res.body.data).toHaveLength(rowsCount)
      expect(res.body.data).toEqual(
        expect.arrayContaining(
          rows.map(r => expect.objectContaining(r.rowData))
        )
      )
    })

    test("Given than multiple tables have multiple rows, get returns the requested ones", async () => {
      await populateRows(2, (await config.createTable())._id)
      const rowsCount = 6
      await populateRows(rowsCount)
      await populateRows(2, (await config.createTable())._id)

      const res = await getAll(postgresTable._id)

      expect(res.status).toBe(200)

      expect(res.body.data).toHaveLength(rowsCount)
    })
  })
})