const Sql = require("../base/sql").default
const { SqlClient } = require("../utils")

const TABLE_NAME = "test"

function endpoint(table: any, operation: any) {
  return {
    datasourceId: "Postgres",
    operation: operation,
    entityId: table || TABLE_NAME,
  }
}

function generateReadJson({
  table,
  fields,
  filters,
  sort,
  paginate,
}: any = {}) {
  return {
    endpoint: endpoint(table || TABLE_NAME, "READ"),
    resource: {
      fields: fields || [],
    },
    filters: filters || {},
    sort: sort || {},
    paginate: paginate || {},
  }
}

function generateCreateJson(table = TABLE_NAME, body = {}) {
  return {
    endpoint: endpoint(table, "CREATE"),
    body,
  }
}

function generateUpdateJson(table = TABLE_NAME, body = {}, filters = {}) {
  return {
    endpoint: endpoint(table, "UPDATE"),
    filters,
    body,
  }
}

function generateDeleteJson(table = TABLE_NAME, filters = {}) {
  return {
    endpoint: endpoint(table, "DELETE"),
    filters,
  }
}

describe("SQL query builder", () => {
  const limit = 500
  const client = SqlClient.POSTGRES
  let sql: any

  beforeEach(() => {
    sql = new Sql(client, limit)
  })

  it("should test a basic read", () => {
    const query = sql._query(generateReadJson())
    expect(query).toEqual({
      bindings: [limit],
      sql: `select * from (select * from "${TABLE_NAME}" limit $1) as "${TABLE_NAME}"`,
    })
  })

  it("should test a read with specific columns", () => {
    const nameProp = `${TABLE_NAME}.name`,
      ageProp = `${TABLE_NAME}.age`
    const query = sql._query(
      generateReadJson({
        fields: [nameProp, ageProp],
      })
    )
    expect(query).toEqual({
      bindings: [limit],
      sql: `select "${TABLE_NAME}"."name" as "${nameProp}", "${TABLE_NAME}"."age" as "${ageProp}" from (select * from "${TABLE_NAME}" limit $1) as "${TABLE_NAME}"`,
    })
  })

  it("should test a where string starts with read", () => {
    const query = sql._query(
      generateReadJson({
        filters: {
          string: {
            name: "John",
          },
        },
      })
    )
    expect(query).toEqual({
      bindings: ["John%", limit],
      sql: `select * from (select * from "${TABLE_NAME}" where "${TABLE_NAME}"."name" ilike $1 limit $2) as "${TABLE_NAME}"`,
    })
  })

  it("should test a where range read", () => {
    const query = sql._query(
      generateReadJson({
        filters: {
          range: {
            age: {
              low: 2,
              high: 10,
            },
          },
        },
      })
    )
    expect(query).toEqual({
      bindings: [2, 10, limit],
      sql: `select * from (select * from "${TABLE_NAME}" where "${TABLE_NAME}"."age" between $1 and $2 limit $3) as "${TABLE_NAME}"`,
    })
  })

  it("should test for multiple IDs with OR", () => {
    const query = sql._query(
      generateReadJson({
        filters: {
          equal: {
            age: 10,
            name: "John",
          },
          allOr: true,
        },
      })
    )
    expect(query).toEqual({
      bindings: [10, "John", limit],
      sql: `select * from (select * from "${TABLE_NAME}" where ("${TABLE_NAME}"."age" = $1) or ("${TABLE_NAME}"."name" = $2) limit $3) as "${TABLE_NAME}"`,
    })
  })

  it("should allow filtering on a related field", () => {
    const query = sql._query(
      generateReadJson({
        filters: {
          equal: {
            age: 10,
            "task.name": "task 1",
          },
        },
      })
    )
    // order of bindings changes because relationship filters occur outside inner query
    expect(query).toEqual({
      bindings: [10, limit, "task 1"],
      sql: `select * from (select * from "${TABLE_NAME}" where "${TABLE_NAME}"."age" = $1 limit $2) as "${TABLE_NAME}" where "task"."name" = $3`,
    })
  })

  it("should test an create statement", () => {
    const query = sql._query(
      generateCreateJson(TABLE_NAME, {
        name: "Michael",
        age: 45,
      })
    )
    expect(query).toEqual({
      bindings: [45, "Michael"],
      sql: `insert into "${TABLE_NAME}" ("age", "name") values ($1, $2) returning *`,
    })
  })

  it("should test an update statement", () => {
    const query = sql._query(
      generateUpdateJson(
        TABLE_NAME,
        {
          name: "John",
        },
        {
          equal: {
            id: 1001,
          },
        }
      )
    )
    expect(query).toEqual({
      bindings: ["John", 1001],
      sql: `update "${TABLE_NAME}" set "name" = $1 where "${TABLE_NAME}"."id" = $2 returning *`,
    })
  })

  it("should test a delete statement", () => {
    const query = sql._query(
      generateDeleteJson(TABLE_NAME, {
        equal: {
          id: 1001,
        },
      })
    )
    expect(query).toEqual({
      bindings: [1001],
      sql: `delete from "${TABLE_NAME}" where "${TABLE_NAME}"."id" = $1 returning *`,
    })
  })

  it("should work with MS-SQL", () => {
    const query = new Sql(SqlClient.MS_SQL, 10)._query(generateReadJson())
    expect(query).toEqual({
      bindings: [10],
      sql: `select * from (select top (@p0) * from [${TABLE_NAME}]) as [${TABLE_NAME}]`,
    })
  })

  it("should work with MySQL", () => {
    const query = new Sql(SqlClient.MY_SQL, 10)._query(generateReadJson())
    expect(query).toEqual({
      bindings: [10],
      sql: `select * from (select * from \`${TABLE_NAME}\` limit ?) as \`${TABLE_NAME}\``,
    })
  })

  it("should use greater than when only low range specified", () => {
    const date = new Date()
    const query = sql._query(
      generateReadJson({
        filters: {
          range: {
            property: {
              low: date,
            },
          },
        },
      })
    )
    expect(query).toEqual({
      bindings: [date, limit],
      sql: `select * from (select * from "${TABLE_NAME}" where "${TABLE_NAME}"."property" > $1 limit $2) as "${TABLE_NAME}"`,
    })
  })

  it("should use less than when only high range specified", () => {
    const date = new Date()
    const query = sql._query(
      generateReadJson({
        filters: {
          range: {
            property: {
              high: date,
            },
          },
        },
      })
    )
    expect(query).toEqual({
      bindings: [date, limit],
      sql: `select * from (select * from "${TABLE_NAME}" where "${TABLE_NAME}"."property" < $1 limit $2) as "${TABLE_NAME}"`,
    })
  })

  it("should use greater than when only low range specified", () => {
    const date = new Date()
    const query = sql._query(
      generateReadJson({
        filters: {
          range: {
            property: {
              low: date,
            },
          },
        },
      })
    )
    expect(query).toEqual({
      bindings: [date, limit],
      sql: `select * from (select * from "${TABLE_NAME}" where "${TABLE_NAME}"."property" > $1 limit $2) as "${TABLE_NAME}"`,
    })
  })

  it("should use AND like expression for MS-SQL when filter is contains", () => {
    const query = new Sql(SqlClient.MS_SQL, 10)._query(
      generateReadJson({
        filters: {
          contains: {
            age: [20, 25],
            name: ["John", "Mary"],
          },
        },
      })
    )
    expect(query).toEqual({
      bindings: [10, "%20%", "%25%", `%"John"%`, `%"Mary"%`],
      sql: `select * from (select top (@p0) * from [${TABLE_NAME}] where (LOWER(${TABLE_NAME}.age) LIKE @p1 AND LOWER(${TABLE_NAME}.age) LIKE @p2) and (LOWER(${TABLE_NAME}.name) LIKE @p3 AND LOWER(${TABLE_NAME}.name) LIKE @p4)) as [${TABLE_NAME}]`,
    })
  })

  it("should use JSON_CONTAINS expression for MySQL when filter is contains", () => {
    const query = new Sql(SqlClient.MY_SQL, 10)._query(
      generateReadJson({
        filters: {
          contains: {
            age: [20],
            name: ["John"],
          },
        },
      })
    )
    expect(query).toEqual({
      bindings: [10],
      sql: `select * from (select * from \`${TABLE_NAME}\` where JSON_CONTAINS(${TABLE_NAME}.age, '[20]') and JSON_CONTAINS(${TABLE_NAME}.name, '["John"]') limit ?) as \`${TABLE_NAME}\``,
    })
  })

  it("should use jsonb operator expression for PostgreSQL when filter is contains", () => {
    const query = new Sql(SqlClient.POSTGRES, 10)._query(
      generateReadJson({
        filters: {
          contains: {
            age: [20],
            name: ["John"],
          },
        },
      })
    )
    expect(query).toEqual({
      bindings: [10],
      sql: `select * from (select * from \"${TABLE_NAME}\" where \"${TABLE_NAME}\".\"age\"::jsonb @> '[20]' and \"${TABLE_NAME}\".\"name\"::jsonb @> '["John"]' limit $1) as \"${TABLE_NAME}\"`,
    })
  })

  it("should use NOT like expression for MS-SQL when filter is notContains", () => {
    const query = new Sql(SqlClient.MS_SQL, 10)._query(
      generateReadJson({
        filters: {
          notContains: {
            age: [20],
            name: ["John"],
          },
        },
      })
    )
    expect(query).toEqual({
      bindings: [10, "%20%", `%"John"%`],
      sql: `select * from (select top (@p0) * from [${TABLE_NAME}] where NOT (LOWER(${TABLE_NAME}.age) LIKE @p1) and NOT (LOWER(${TABLE_NAME}.name) LIKE @p2)) as [${TABLE_NAME}]`,
    })
  })

  it("should use NOT JSON_CONTAINS expression for MySQL when filter is notContains", () => {
    const query = new Sql(SqlClient.MY_SQL, 10)._query(
      generateReadJson({
        filters: {
          notContains: {
            age: [20],
            name: ["John"],
          },
        },
      })
    )
    expect(query).toEqual({
      bindings: [10],
      sql: `select * from (select * from \`${TABLE_NAME}\` where NOT JSON_CONTAINS(${TABLE_NAME}.age, '[20]') and NOT JSON_CONTAINS(${TABLE_NAME}.name, '["John"]') limit ?) as \`${TABLE_NAME}\``,
    })
  })

  it("should use jsonb operator NOT expression for PostgreSQL when filter is notContains", () => {
    const query = new Sql(SqlClient.POSTGRES, 10)._query(
      generateReadJson({
        filters: {
          notContains: {
            age: [20],
            name: ["John"],
          },
        },
      })
    )
    expect(query).toEqual({
      bindings: [10],
      sql: `select * from (select * from \"${TABLE_NAME}\" where NOT \"${TABLE_NAME}\".\"age\"::jsonb @> '[20]' and NOT \"${TABLE_NAME}\".\"name\"::jsonb @> '["John"]' limit $1) as \"${TABLE_NAME}\"`,
    })
  })

  it("should use OR like expression for MS-SQL when filter is containsAny", () => {
    const query = new Sql(SqlClient.MS_SQL, 10)._query(
      generateReadJson({
        filters: {
          containsAny: {
            age: [20, 25],
            name: ["John", "Mary"],
          },
        },
      })
    )
    expect(query).toEqual({
      bindings: [10, "%20%", "%25%", `%"John"%`, `%"Mary"%`],
      sql: `select * from (select top (@p0) * from [${TABLE_NAME}] where (LOWER(${TABLE_NAME}.age) LIKE @p1 OR LOWER(${TABLE_NAME}.age) LIKE @p2) and (LOWER(${TABLE_NAME}.name) LIKE @p3 OR LOWER(${TABLE_NAME}.name) LIKE @p4)) as [${TABLE_NAME}]`,
    })
  })

  it("should use JSON_OVERLAPS expression for MySQL when filter is containsAny", () => {
    const query = new Sql(SqlClient.MY_SQL, 10)._query(
      generateReadJson({
        filters: {
          containsAny: {
            age: [20, 25],
            name: ["John", "Mary"],
          },
        },
      })
    )
    expect(query).toEqual({
      bindings: [10],
      sql: `select * from (select * from \`${TABLE_NAME}\` where JSON_OVERLAPS(${TABLE_NAME}.age, '[20,25]') and JSON_OVERLAPS(${TABLE_NAME}.name, '["John","Mary"]') limit ?) as \`${TABLE_NAME}\``,
    })
  })

  it("should use ?| operator expression for PostgreSQL when filter is containsAny", () => {
    const query = new Sql(SqlClient.POSTGRES, 10)._query(
      generateReadJson({
        filters: {
          containsAny: {
            age: [20, 25],
            name: ["John", "Mary"],
          },
        },
      })
    )
    expect(query).toEqual({
      bindings: [10],
      sql: `select * from (select * from \"${TABLE_NAME}\" where \"${TABLE_NAME}\".\"age\"::jsonb ?| array [20,25] and \"${TABLE_NAME}\".\"name\"::jsonb ?| array ['John','Mary'] limit $1) as \"${TABLE_NAME}\"`,
    })
  })
})
