import { describe, expect, test } from "@jest/globals"
import { z } from "zod"

import { parseSearchParams, serializeSearchParams } from "./searchParams"

describe("Parse search params", () => {
  test("with flat values", () => {
    const searchParams = getSearchParams("city=stockholm&hotel=123")
    const result = parseSearchParams(searchParams)

    expect(result).toEqual({
      city: "stockholm",
      hotel: "123",
    })
  })

  test("with comma separated array", () => {
    const searchParams = getSearchParams(
      "filter=1831,1383,971,1607&packages=ABC,XYZ"
    )
    const result = parseSearchParams(searchParams, {
      typeHints: {
        packages: "COMMA_SEPARATED_ARRAY",
        filter: "COMMA_SEPARATED_ARRAY",
      },
    })

    expect(result).toEqual({
      filter: ["1831", "1383", "971", "1607"],
      packages: ["ABC", "XYZ"],
    })
  })

  test("with comma separated array with single value", () => {
    const searchParams = getSearchParams(
      "details.packages=ABC&filter=1831&rooms[0].packages=XYZ"
    )
    const result = parseSearchParams(searchParams, {
      typeHints: {
        filter: "COMMA_SEPARATED_ARRAY",
        packages: "COMMA_SEPARATED_ARRAY",
      },
    })

    expect(result).toEqual({
      filter: ["1831"],
      details: {
        packages: ["ABC"],
      },
      rooms: [
        {
          packages: ["XYZ"],
        },
      ],
    })
  })

  test("with nested object", () => {
    const searchParams = getSearchParams(
      "room.details.adults=1&room.ratecode=ABC&room.details.children=2&room.filters=1,2,3,4"
    )
    const result = parseSearchParams(searchParams, {
      typeHints: {
        filters: "COMMA_SEPARATED_ARRAY",
      },
    })
    expect(result).toEqual({
      room: {
        ratecode: "ABC",
        filters: ["1", "2", "3", "4"],
        details: {
          adults: "1",
          children: "2",
        },
      },
    })
  })

  test("with array of objects", () => {
    const searchParams = getSearchParams(
      "room[0].adults=1&room[0].ratecode=ABC&room[1].adults=2&room[1].ratecode=DEF"
    )

    const result = parseSearchParams(searchParams)
    expect(result).toEqual({
      room: [
        {
          adults: "1",
          ratecode: "ABC",
        },
        {
          adults: "2",
          ratecode: "DEF",
        },
      ],
    })
  })

  test("with array defined out of order", () => {
    const searchParams = getSearchParams("room[1].adults=1&room[0].adults=2")

    const result = parseSearchParams(searchParams)
    expect(result).toEqual({
      room: [
        {
          adults: "2",
        },
        {
          adults: "1",
        },
      ],
    })
  })

  test("with nested array of objects", () => {
    const searchParams = getSearchParams(
      "room[0].adults=1&room[0].child[0].age=2&room[1].adults=2&room[1].child[0].age=3"
    )

    const result = parseSearchParams(searchParams)

    expect(result).toEqual({
      room: [
        {
          adults: "1",
          child: [
            {
              age: "2",
            },
          ],
        },
        {
          adults: "2",
          child: [
            {
              age: "3",
            },
          ],
        },
      ],
    })
  })

  test("can handle array syntax with primitive values", () => {
    const searchParams = getSearchParams("room[1]=1&room[0]=2")
    const result = parseSearchParams(searchParams)
    expect(result).toEqual({
      room: ["2", "1"],
    })
  })

  test("can rename search param keys", () => {
    const searchParams = getSearchParams(
      "city=stockholm&hotel=123&room[0].adults=1&room[0].child[0].age=2&room[1].adults=2&room[1].child[0].age=3"
    )
    const result = parseSearchParams(searchParams, {
      keyRenameMap: {
        hotel: "hotelId",
        room: "rooms",
        age: "childAge",
      },
    })

    expect(result).toEqual({
      city: "stockholm",
      hotelId: "123",
      rooms: [
        {
          adults: "1",
          child: [
            {
              childAge: "2",
            },
          ],
        },
        {
          adults: "2",
          child: [
            {
              childAge: "3",
            },
          ],
        },
      ],
    })
  })

  test("with schema validation", () => {
    const searchParams = getSearchParams(
      "room[0].adults=1&room[0].child[0].age=2&room[1].adults=2&room[1].child[0].age=3"
    )

    const result = parseSearchParams(searchParams, {
      schema: z.object({
        room: z.array(
          z.object({
            adults: z.string(),
            child: z.array(
              z.object({
                age: z.string(),
              })
            ),
          })
        ),
      }),
    })
    expect(result).toEqual({
      room: [
        {
          adults: "1",
          child: [
            {
              age: "2",
            },
          ],
        },
        {
          adults: "2",
          child: [
            {
              age: "3",
            },
          ],
        },
      ],
    })
  })

  test("throws when schema validation fails", () => {
    const searchParams = getSearchParams("city=stockholm")

    expect(() =>
      parseSearchParams(searchParams, {
        schema: z.object({
          city: z.string(),
          hotel: z.string(),
        }),
      })
    ).toThrow()
  })

  test("with value coercion", () => {
    const searchParams = getSearchParams(
      "room[0].adults=1&room[0].enabled=true"
    )

    const result = parseSearchParams(searchParams, {
      schema: z.object({
        room: z.array(
          z.object({
            adults: z.coerce.number(),
            enabled: z.coerce.boolean(),
          })
        ),
      }),
    })
    expect(result).toEqual({
      room: [
        {
          adults: 1,
          enabled: true,
        },
      ],
    })
  })
})

describe("Serialize search params", () => {
  test("with flat values", () => {
    const obj = {
      city: "stockholm",
      hotel: "123",
    }
    const result = serializeSearchParams(obj)

    expect(decodeURIComponent(result.toString())).toEqual(
      "city=stockholm&hotel=123"
    )
  })

  test("with comma separated array", () => {
    const obj = {
      filter: ["1831", "1383", "971", "1607"],
    }
    const result = serializeSearchParams(obj, {
      typeHints: {
        filter: "COMMA_SEPARATED_ARRAY",
      },
    })

    expect(decodeURIComponent(result.toString())).toEqual(
      "filter=1831,1383,971,1607"
    )
  })

  test("with comma separated array with single value", () => {
    const obj = {
      details: {
        packages: ["ABC"],
      },
      filter: ["1831"],
      rooms: [
        {
          packages: ["XYZ"],
        },
      ],
    }
    const result = serializeSearchParams(obj, {
      typeHints: {
        filter: "COMMA_SEPARATED_ARRAY",
        packages: "COMMA_SEPARATED_ARRAY",
      },
    })

    expect(decodeURIComponent(result.toString())).toEqual(
      "details.packages=ABC&filter=1831&rooms[0].packages=XYZ"
    )
  })

  test("with nested object", () => {
    const obj = {
      room: {
        ratecode: "ABC",
        filters: ["1", "2", "3", "4"],
        details: {
          adults: "1",
          children: "2",
        },
      },
    }
    const result = serializeSearchParams(obj, {
      typeHints: {
        filters: "COMMA_SEPARATED_ARRAY",
      },
    })

    expect(decodeURIComponent(result.toString())).toEqual(
      "room.ratecode=ABC&room.filters=1,2,3,4&room.details.adults=1&room.details.children=2"
    )
  })

  test("with array of objects", () => {
    const obj = {
      room: [
        {
          adults: "1",
          ratecode: "ABC",
        },
        {
          adults: "2",
          ratecode: "DEF",
        },
      ],
    }
    const result = serializeSearchParams(obj)

    expect(decodeURIComponent(result.toString())).toEqual(
      "room[0].adults=1&room[0].ratecode=ABC&room[1].adults=2&room[1].ratecode=DEF"
    )
  })

  test("with nested array of objects", () => {
    const obj = {
      room: [
        {
          adults: "1",
          child: [
            {
              age: "2",
            },
          ],
        },
        {
          adults: "2",
          child: [
            {
              age: "3",
            },
          ],
        },
      ],
    }
    const result = serializeSearchParams(obj)

    expect(decodeURIComponent(result.toString())).toEqual(
      "room[0].adults=1&room[0].child[0].age=2&room[1].adults=2&room[1].child[0].age=3"
    )
  })

  test("can handle array syntax with primitive values", () => {
    const obj = {
      room: ["2", "1"],
    }
    const result = serializeSearchParams(obj)

    expect(decodeURIComponent(result.toString())).toEqual("room[0]=2&room[1]=1")
  })

  test("can rename search param keys", () => {
    const obj = {
      city: "stockholm",
      hotelId: "123",
      rooms: [
        {
          adults: "1",
          child: [
            {
              childAge: "2",
            },
          ],
        },
        {
          adults: "2",
          child: [
            {
              childAge: "3",
            },
          ],
        },
      ],
    }
    const result = serializeSearchParams(obj, {
      keyRenameMap: {
        hotelId: "hotel",
        rooms: "room",
        childAge: "age",
      },
    })

    expect(decodeURIComponent(result.toString())).toEqual(
      "city=stockholm&hotel=123&room[0].adults=1&room[0].child[0].age=2&room[1].adults=2&room[1].child[0].age=3"
    )
  })

  test("with initial search params", () => {
    const initialSearchParams = new URLSearchParams("city=stockholm&hotel=123")
    const obj = {
      hotel: "456",
      filter: ["1831", "1383"],
      packages: ["ABC"],
    }
    const result = serializeSearchParams(obj, {
      initialSearchParams,
      typeHints: {
        packages: "COMMA_SEPARATED_ARRAY",
      },
    })

    expect(decodeURIComponent(result.toString())).toEqual(
      "city=stockholm&hotel=456&filter[0]=1831&filter[1]=1383&packages=ABC"
    )
  })
})

describe("Parse serialized search params", () => {
  test("should return the same object", () => {
    const obj = {
      city: "stockholm",
      hotelId: "123",
      filter: ["1831", "1383", "971", "1607"],
      details: {
        packages: ["ABC"],
      },
      rooms: [
        {
          packages: ["XYZ"],
        },
      ],
    }

    const searchParams = serializeSearchParams(obj, {
      keyRenameMap: {
        hotelId: "hotel",
        rooms: "room",
      },
      typeHints: {
        filter: "COMMA_SEPARATED_ARRAY",
        packages: "COMMA_SEPARATED_ARRAY",
      },
    })

    const searchParamsObj = searchParamsToObject(searchParams)
    const result = parseSearchParams(searchParamsObj, {
      keyRenameMap: {
        hotel: "hotelId",
        room: "rooms",
      },
      typeHints: {
        filter: "COMMA_SEPARATED_ARRAY",
        packages: "COMMA_SEPARATED_ARRAY",
      },
    })

    expect(result).toEqual(obj)
  })
})

// Simulates what Next does behind the scenes for search params
const getSearchParams = (input: string) => {
  const searchParams = new URLSearchParams(input)
  return searchParamsToObject(searchParams)
}
const searchParamsToObject = (searchParams: URLSearchParams) => {
  const obj: Record<string, any> = {}
  for (const [key, value] of searchParams.entries()) {
    obj[key] = value
  }
  return obj
}
