// Copyright 2018 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.

package sqlbuilder

import (
	"strings"
	"testing"

	"github.com/huandu/go-assert"
)

type TestPair struct {
	Expected string
	Actual   string
}

func newTestPair(expected string, fn func(c *Cond) string) *TestPair {
	cond := newCond()
	format := fn(cond)
	sql, _ := cond.Args.CompileWithFlavor(format, PostgreSQL)
	return &TestPair{
		Expected: expected,
		Actual:   sql,
	}
}

func TestCond(t *testing.T) {
	a := assert.New(t)
	cases := []*TestPair{
		newTestPair("$a = $1", func(c *Cond) string { return c.Equal("$a", 123) }),
		newTestPair("$b = $1", func(c *Cond) string { return c.E("$b", 123) }),
		newTestPair("$c = $1", func(c *Cond) string { return c.EQ("$c", 123) }),
		newTestPair("$a <> $1", func(c *Cond) string { return c.NotEqual("$a", 123) }),
		newTestPair("$b <> $1", func(c *Cond) string { return c.NE("$b", 123) }),
		newTestPair("$c <> $1", func(c *Cond) string { return c.NEQ("$c", 123) }),
		newTestPair("$a > $1", func(c *Cond) string { return c.GreaterThan("$a", 123) }),
		newTestPair("$b > $1", func(c *Cond) string { return c.G("$b", 123) }),
		newTestPair("$c > $1", func(c *Cond) string { return c.GT("$c", 123) }),
		newTestPair("$a >= $1", func(c *Cond) string { return c.GreaterEqualThan("$a", 123) }),
		newTestPair("$b >= $1", func(c *Cond) string { return c.GE("$b", 123) }),
		newTestPair("$c >= $1", func(c *Cond) string { return c.GTE("$c", 123) }),
		newTestPair("$a < $1", func(c *Cond) string { return c.LessThan("$a", 123) }),
		newTestPair("$b < $1", func(c *Cond) string { return c.L("$b", 123) }),
		newTestPair("$c < $1", func(c *Cond) string { return c.LT("$c", 123) }),
		newTestPair("$a <= $1", func(c *Cond) string { return c.LessEqualThan("$a", 123) }),
		newTestPair("$b <= $1", func(c *Cond) string { return c.LE("$b", 123) }),
		newTestPair("$c <= $1", func(c *Cond) string { return c.LTE("$c", 123) }),
		newTestPair("$a IN ($1, $2, $3)", func(c *Cond) string { return c.In("$a", 1, 2, 3) }),
		newTestPair("0 = 1", func(c *Cond) string { return c.In("$a") }),
		newTestPair("$a NOT IN ($1, $2, $3)", func(c *Cond) string { return c.NotIn("$a", 1, 2, 3) }),
		newTestPair("0 = 0", func(c *Cond) string { return c.NotIn("$a") }),
		newTestPair("$a LIKE $1", func(c *Cond) string { return c.Like("$a", "%Huan%") }),
		newTestPair("$a ILIKE $1", func(c *Cond) string { return c.ILike("$a", "%Huan%") }),
		newTestPair("$a NOT LIKE $1", func(c *Cond) string { return c.NotLike("$a", "%Huan%") }),
		newTestPair("$a NOT ILIKE $1", func(c *Cond) string { return c.NotILike("$a", "%Huan%") }),
		newTestPair("$a IS NULL", func(c *Cond) string { return c.IsNull("$a") }),
		newTestPair("$a IS NOT NULL", func(c *Cond) string { return c.IsNotNull("$a") }),
		newTestPair("$a BETWEEN $1 AND $2", func(c *Cond) string { return c.Between("$a", 123, 456) }),
		newTestPair("$a NOT BETWEEN $1 AND $2", func(c *Cond) string { return c.NotBetween("$a", 123, 456) }),
		newTestPair("NOT 1 = 1", func(c *Cond) string { return c.Not("1 = 1") }),
		newTestPair("EXISTS ($1)", func(c *Cond) string { return c.Exists(1) }),
		newTestPair("NOT EXISTS ($1)", func(c *Cond) string { return c.NotExists(1) }),
		newTestPair("$a > ANY ($1, $2)", func(c *Cond) string { return c.Any("$a", ">", 1, 2) }),
		newTestPair("0 = 1", func(c *Cond) string { return c.Any("$a", ">") }),
		newTestPair("$a < ALL ($1)", func(c *Cond) string { return c.All("$a", "<", 1) }),
		newTestPair("0 = 1", func(c *Cond) string { return c.All("$a", "<") }),
		newTestPair("$a > SOME ($1, $2, $3)", func(c *Cond) string { return c.Some("$a", ">", 1, 2, 3) }),
		newTestPair("0 = 1", func(c *Cond) string { return c.Some("$a", ">") }),
		newTestPair("$a IS DISTINCT FROM $1", func(c *Cond) string { return c.IsDistinctFrom("$a", 1) }),
		newTestPair("$a IS NOT DISTINCT FROM $1", func(c *Cond) string { return c.IsNotDistinctFrom("$a", 1) }),
		newTestPair("$1", func(c *Cond) string { return c.Var(123) }),
	}

	for _, f := range cases {
		a.Equal(f.Actual, f.Expected)
	}
}

func TestOrCond(t *testing.T) {
	a := assert.New(t)
	cases := []*TestPair{
		newTestPair("(1 = 1 OR 2 = 2 OR 3 = 3)", func(c *Cond) string { return c.Or("1 = 1", "2 = 2", "3 = 3") }),

		newTestPair("(1 = 1 OR 2 = 2)", func(c *Cond) string { return c.Or("", "1 = 1", "2 = 2") }),
		newTestPair("(1 = 1 OR 2 = 2)", func(c *Cond) string { return c.Or("1 = 1", "2 = 2", "") }),
		newTestPair("(1 = 1 OR 2 = 2)", func(c *Cond) string { return c.Or("1 = 1", "", "2 = 2") }),

		newTestPair("(1 = 1)", func(c *Cond) string { return c.Or("1 = 1", "", "") }),
		newTestPair("(1 = 1)", func(c *Cond) string { return c.Or("", "1 = 1", "") }),
		newTestPair("(1 = 1)", func(c *Cond) string { return c.Or("", "", "1 = 1") }),
		newTestPair("(1 = 1)", func(c *Cond) string { return c.Or("1 = 1") }),

		{Expected: "", Actual: newCond().Or("")},
		{Expected: "", Actual: newCond().Or()},
		{Expected: "", Actual: newCond().Or("", "", "")},
	}

	for _, f := range cases {
		a.Equal(f.Actual, f.Expected)
	}
}

func TestAndCond(t *testing.T) {
	a := assert.New(t)
	cases := []*TestPair{
		newTestPair("(1 = 1 AND 2 = 2 AND 3 = 3)", func(c *Cond) string { return c.And("1 = 1", "2 = 2", "3 = 3") }),

		newTestPair("(1 = 1 AND 2 = 2)", func(c *Cond) string { return c.And("", "1 = 1", "2 = 2") }),
		newTestPair("(1 = 1 AND 2 = 2)", func(c *Cond) string { return c.And("1 = 1", "2 = 2", "") }),
		newTestPair("(1 = 1 AND 2 = 2)", func(c *Cond) string { return c.And("1 = 1", "", "2 = 2") }),

		newTestPair("(1 = 1)", func(c *Cond) string { return c.And("1 = 1", "", "") }),
		newTestPair("(1 = 1)", func(c *Cond) string { return c.And("", "1 = 1", "") }),
		newTestPair("(1 = 1)", func(c *Cond) string { return c.And("", "", "1 = 1") }),
		newTestPair("(1 = 1)", func(c *Cond) string { return c.And("1 = 1") }),

		{Expected: "", Actual: newCond().And("")},
		{Expected: "", Actual: newCond().And()},
		{Expected: "", Actual: newCond().And("", "", "")},
	}

	for _, f := range cases {
		a.Equal(f.Actual, f.Expected)
	}
}

func TestEmptyCond(t *testing.T) {
	a := assert.New(t)
	cases := []string{
		newCond().Equal("", 123),
		newCond().NotEqual("", 123),
		newCond().GreaterThan("", 123),
		newCond().GreaterEqualThan("", 123),
		newCond().LessThan("", 123),
		newCond().LessEqualThan("", 123),
		newCond().In("", 1, 2, 3),
		newCond().NotIn("", 1, 2, 3),
		newCond().Like("", "%Huan%"),
		newCond().ILike("", "%Huan%"),
		newCond().NotLike("", "%Huan%"),
		newCond().NotILike("", "%Huan%"),
		newCond().IsNull(""),
		newCond().IsNotNull(""),
		newCond().Between("", 123, 456),
		newCond().NotBetween("", 123, 456),
		newCond().Not(""),

		newCond().Any("", "", 1, 2),
		newCond().Any("", ">", 1, 2),
		newCond().Any("$a", "", 1, 2),

		newCond().All("", "", 1),
		newCond().All("", ">", 1),
		newCond().All("$a", "", 1),

		newCond().Some("", "", 1, 2, 3),
		newCond().Some("", ">", 1, 2, 3),
		newCond().Some("$a", "", 1, 2, 3),

		newCond().IsDistinctFrom("", 1),
		newCond().IsNotDistinctFrom("", 1),
	}

	expected := ""
	for _, actual := range cases {
		a.Equal(actual, expected)
	}
}

func TestCondWithFlavor(t *testing.T) {
	a := assert.New(t)
	cond := &Cond{
		Args: &Args{},
	}
	format := strings.Join([]string{
		cond.ILike("f1", 1),
		cond.NotILike("f2", 2),
		cond.IsDistinctFrom("f3", 3),
		cond.IsNotDistinctFrom("f4", 4),
	}, "\n")
	expectedResults := map[Flavor]string{
		PostgreSQL: `f1 ILIKE $1
f2 NOT ILIKE $2
f3 IS DISTINCT FROM $3
f4 IS NOT DISTINCT FROM $4`,
		MySQL: `LOWER(f1) LIKE LOWER(?)
LOWER(f2) NOT LIKE LOWER(?)
NOT f3 <=> ?
f4 <=> ?`,
		SQLite: `LOWER(f1) LIKE LOWER(?)
LOWER(f2) NOT LIKE LOWER(?)
f3 IS DISTINCT FROM ?
f4 IS NOT DISTINCT FROM ?`,
		Presto: `LOWER(f1) LIKE LOWER(?)
LOWER(f2) NOT LIKE LOWER(?)
CASE WHEN f3 IS NULL AND ? IS NULL THEN 0 WHEN f3 IS NOT NULL AND ? IS NOT NULL AND f3 = ? THEN 0 ELSE 1 END = 1
CASE WHEN f4 IS NULL AND ? IS NULL THEN 1 WHEN f4 IS NOT NULL AND ? IS NOT NULL AND f4 = ? THEN 1 ELSE 0 END = 1`,
		ClickHouse: `f1 ILIKE ?
f2 NOT ILIKE ?
CASE WHEN f3 IS NULL AND ? IS NULL THEN 0 WHEN f3 IS NOT NULL AND ? IS NOT NULL AND f3 = ? THEN 0 ELSE 1 END = 1
CASE WHEN f4 IS NULL AND ? IS NULL THEN 1 WHEN f4 IS NOT NULL AND ? IS NOT NULL AND f4 = ? THEN 1 ELSE 0 END = 1`,
	}

	for flavor, expected := range expectedResults {
		actual, _ := cond.Args.CompileWithFlavor(format, flavor)
		a.Equal(actual, expected)
	}
}

func TestCondExpr(t *testing.T) {
	a := assert.New(t)
	cond := &Cond{
		Args: &Args{},
	}
	sb1 := Select("1 = 1")
	sb2 := Select("FALSE")
	formats := []string{
		cond.And(),
		cond.Or(),
		cond.And(cond.Var(sb1), cond.Var(sb2)),
		cond.Or(cond.Var(sb1), cond.Var(sb2)),
		cond.Not(cond.Or(cond.Var(sb1), cond.And(cond.Var(sb1), cond.Var(sb2)))),
	}
	expectResults := []string{
		"",
		"",
		"(SELECT 1 = 1 AND SELECT FALSE)",
		"(SELECT 1 = 1 OR SELECT FALSE)",
		"NOT (SELECT 1 = 1 OR (SELECT 1 = 1 AND SELECT FALSE))",
	}

	for i, expected := range expectResults {
		actual, values := cond.Args.Compile(formats[i])
		a.Equal(len(values), 0)
		a.Equal(actual, expected)
	}
}

func TestCondMisuse(t *testing.T) {
	a := assert.New(t)

	cond := NewCond()
	sb := Select("*").
		From("t1").
		Where(cond.Equal("a", 123))
	sql, args := sb.Build()

	a.Equal(sql, "SELECT * FROM t1 WHERE /* INVALID ARG $256 */")
	a.Equal(args, nil)
}

func newCond() *Cond {
	args := &Args{}
	return &Cond{
		Args: args,
	}
}
