package pg_test

import (
	"database/sql"
	"database/sql/driver"
	"encoding/json"
	"fmt"
	"math"
	"net"
	"reflect"
	"testing"
	"time"

	"github.com/go-pg/pg"
	"github.com/go-pg/pg/orm"
	"github.com/go-pg/pg/types"
)

type JSONMap map[string]interface{}

func (m *JSONMap) Scan(b interface{}) error {
	if b == nil {
		*m = nil
		return nil
	}
	return json.Unmarshal(b.([]byte), m)
}

func (m JSONMap) Value() (driver.Value, error) {
	b, err := json.Marshal(m)
	if err != nil {
		return nil, err
	}
	return string(b), nil
}

type (
	StringSlice  []string
	IntSlice     []int
	Int64Slice   []int64
	Float64Slice []float64
)

type Struct struct {
	Foo string
}

type conversionTest struct {
	i                int
	src, dst, wanted interface{}
	pgtype           string

	wanterr  string
	wantnil  bool
	wantzero bool
}

func unwrap(v interface{}) interface{} {
	if arr, ok := v.(*types.Array); ok {
		return arr.Value()
	}
	if hstore, ok := v.(*types.Hstore); ok {
		return hstore.Value()
	}
	return v
}

func deref(vi interface{}) interface{} {
	v := reflect.ValueOf(vi)
	for v.Kind() == reflect.Ptr {
		v = v.Elem()
	}
	if v.IsValid() {
		return v.Interface()
	}
	return nil
}

func zero(v interface{}) interface{} {
	return reflect.Zero(reflect.ValueOf(v).Elem().Type()).Interface()
}

func (test *conversionTest) String() string {
	return fmt.Sprintf("#%d src=%#v dst=%#v", test.i, test.src, test.dst)
}

func (test *conversionTest) Assert(t *testing.T, err error) {
	if test.wanterr != "" {
		if err == nil || err.Error() != test.wanterr {
			t.Fatalf("got error %q, wanted %q (%s)", err, test.wanterr, test)
		}
		return
	}

	if err != nil {
		t.Fatalf("got error %q, wanted nil (%s)", err, test)
	}

	dst := reflect.Indirect(reflect.ValueOf(unwrap(test.dst))).Interface()

	if test.wantnil {
		dstValue := reflect.ValueOf(dst)
		if !dstValue.IsValid() {
			return
		}
		if dstValue.IsNil() {
			return
		}
		t.Fatalf("got %#v, wanted nil (%s)", dst, test)
		return
	}

	// Remove any intermediate pointers to compare values.
	dst = deref(unwrap(dst))
	src := deref(unwrap(test.src))

	if test.wantzero {
		dstValue := reflect.ValueOf(dst)
		switch dstValue.Kind() {
		case reflect.Slice, reflect.Map:
			if dstValue.IsNil() {
				t.Fatalf("got nil, wanted zero value")
			}
			if dstValue.Len() != 0 {
				t.Fatalf("got %d items, wanted 0", dstValue.Len())
			}
		default:
			zero := zero(test.dst)
			if dst != zero {
				t.Fatalf("%#v != %#v (%s)", dst, zero, test)
			}
		}
		return
	}

	if dstTime, ok := dst.(time.Time); ok {
		srcTime := src.(time.Time)
		if dstTime.Unix() != srcTime.Unix() {
			t.Fatalf("%#v != %#v", dstTime, srcTime)
		}
		return
	}

	if dstTimes, ok := dst.([]time.Time); ok {
		srcTimes := src.([]time.Time)
		for i, dstTime := range dstTimes {
			srcTime := srcTimes[i]
			if dstTime.Unix() != srcTime.Unix() {
				t.Fatalf("%#v != %#v", dstTime, srcTime)
			}
		}
		return
	}

	wanted := test.wanted
	if wanted == nil {
		wanted = src
	}
	if !reflect.DeepEqual(dst, wanted) {
		t.Fatalf("%#v != %#v (%s)", dst, wanted, test)
	}
}

func conversionTests() []conversionTest {
	return []conversionTest{
		{src: nil, dst: nil, wanterr: "pg: Scan(nil)"},
		{src: nil, dst: new(uintptr), wanterr: "pg: Scan(unsupported uintptr)"},

		{src: nil, dst: true, pgtype: "bool", wanterr: "pg: Scan(non-pointer bool)"},
		{src: nil, dst: new(*bool), pgtype: "bool", wantnil: true},
		{src: nil, dst: new(bool), pgtype: "bool", wantzero: true},
		{src: true, dst: new(bool), pgtype: "bool"},
		{src: true, dst: new(*bool), pgtype: "bool"},
		{src: 1, dst: new(bool), wanted: true},

		{src: nil, dst: "", pgtype: "text", wanterr: "pg: Scan(non-pointer string)"},
		{src: nil, dst: new(string), pgtype: "text", wantzero: true},
		{src: nil, dst: new(*string), pgtype: "text", wantnil: true},
		{src: "hello world", dst: new(string), pgtype: "text"},
		{src: "hello world", dst: new(*string), pgtype: "text"},
		{src: "'\"\000", dst: new(string), wanted: `'"`, pgtype: "text"},

		{src: nil, dst: []byte(nil), pgtype: "bytea", wanterr: "pg: Scan(non-pointer []uint8)"},
		{src: nil, dst: new([]byte), pgtype: "bytea", wantnil: true},
		{src: []byte("hello world\000"), dst: new([]byte), pgtype: "bytea"},
		{src: []byte{}, dst: new([]byte), pgtype: "bytea", wantzero: true},

		{src: nil, dst: int8(0), pgtype: "smallint", wanterr: "pg: Scan(non-pointer int8)"},
		{src: nil, dst: new(int8), pgtype: "smallint", wantzero: true},
		{src: int8(math.MaxInt8), dst: new(int8), pgtype: "smallint"},
		{src: int8(math.MaxInt8), dst: new(*int8), pgtype: "smallint"},
		{src: int8(math.MinInt8), dst: new(int8), pgtype: "smallint"},

		{src: nil, dst: int16(0), pgtype: "smallint", wanterr: "pg: Scan(non-pointer int16)"},
		{src: nil, dst: new(int16), pgtype: "smallint", wantzero: true},
		{src: int16(math.MaxInt16), dst: new(int16), pgtype: "smallint"},
		{src: int16(math.MaxInt16), dst: new(*int16), pgtype: "smallint"},
		{src: int16(math.MinInt16), dst: new(int16), pgtype: "smallint"},

		{src: nil, dst: int32(0), pgtype: "int", wanterr: "pg: Scan(non-pointer int32)"},
		{src: nil, dst: new(int32), pgtype: "int", wantzero: true},
		{src: int32(math.MaxInt32), dst: new(int32), pgtype: "int"},
		{src: int32(math.MaxInt32), dst: new(*int32), pgtype: "int"},
		{src: int32(math.MinInt32), dst: new(int32), pgtype: "int"},

		{src: nil, dst: int64(0), pgtype: "bigint", wanterr: "pg: Scan(non-pointer int64)"},
		{src: nil, dst: new(int64), pgtype: "bigint", wantzero: true},
		{src: int64(math.MaxInt64), dst: new(int64), pgtype: "bigint"},
		{src: int64(math.MaxInt64), dst: new(*int64), pgtype: "bigint"},
		{src: int64(math.MinInt64), dst: new(int64), pgtype: "bigint"},

		{src: nil, dst: int(0), pgtype: "bigint", wanterr: "pg: Scan(non-pointer int)"},
		{src: nil, dst: new(int), pgtype: "bigint", wantzero: true},
		{src: int(math.MaxInt64), dst: new(int), pgtype: "bigint"},
		{src: int(math.MaxInt64), dst: new(*int), pgtype: "bigint"},
		{src: int(math.MinInt32), dst: new(int), pgtype: "bigint"},

		{src: nil, dst: uint8(0), pgtype: "smallint", wanterr: "pg: Scan(non-pointer uint8)"},
		{src: nil, dst: new(uint8), pgtype: "smallint", wantzero: true},
		{src: uint8(math.MaxUint8), dst: new(uint8), pgtype: "smallint"},
		{src: uint8(math.MaxUint8), dst: new(*uint8), pgtype: "smallint"},

		{src: nil, dst: uint16(0), pgtype: "smallint", wanterr: "pg: Scan(non-pointer uint16)"},
		{src: nil, dst: new(uint16), pgtype: "smallint", wantzero: true},
		{src: uint16(math.MaxUint16), dst: new(uint16), pgtype: "int"},
		{src: uint16(math.MaxUint16), dst: new(*uint16), pgtype: "int"},

		{src: nil, dst: uint32(0), pgtype: "bigint", wanterr: "pg: Scan(non-pointer uint32)"},
		{src: nil, dst: new(uint32), pgtype: "bigint", wantzero: true},
		{src: uint32(math.MaxUint32), dst: new(uint32), pgtype: "bigint"},
		{src: uint32(math.MaxUint32), dst: new(*uint32), pgtype: "bigint"},

		{src: nil, dst: uint64(0), pgtype: "bigint", wanterr: "pg: Scan(non-pointer uint64)"},
		{src: nil, dst: new(uint64), pgtype: "bigint", wantzero: true},
		{src: uint64(math.MaxUint64), dst: new(uint64)},
		{src: uint64(math.MaxUint64), dst: new(*uint64)},
		{src: uint64(math.MaxUint32), dst: new(uint64), pgtype: "bigint"},

		{src: nil, dst: uint(0), pgtype: "smallint", wanterr: "pg: Scan(non-pointer uint)"},
		{src: nil, dst: new(uint), pgtype: "bigint", wantzero: true},
		{src: uint(math.MaxUint64), dst: new(uint)},
		{src: uint(math.MaxUint64), dst: new(*uint)},
		{src: uint(math.MaxUint32), dst: new(uint), pgtype: "bigint"},

		{src: nil, dst: float32(0), pgtype: "decimal", wanterr: "pg: Scan(non-pointer float32)"},
		{src: nil, dst: new(float32), pgtype: "decimal", wantzero: true},
		{src: float32(math.MaxFloat32), dst: new(float32), pgtype: "decimal"},
		{src: float32(math.MaxFloat32), dst: new(*float32), pgtype: "decimal"},
		{src: float32(math.SmallestNonzeroFloat32), dst: new(float32), pgtype: "decimal"},

		{src: nil, dst: float64(0), pgtype: "decimal", wanterr: "pg: Scan(non-pointer float64)"},
		{src: nil, dst: new(float64), pgtype: "decimal", wantzero: true},
		{src: float64(math.MaxFloat64), dst: new(float64), pgtype: "decimal"},
		{src: float64(math.MaxFloat64), dst: new(*float64), pgtype: "decimal"},
		{src: float64(math.SmallestNonzeroFloat64), dst: new(float64), pgtype: "decimal"},

		{src: nil, dst: []int(nil), pgtype: "jsonb", wanterr: "pg: Scan(non-pointer []int)"},
		{src: nil, dst: new([]int), pgtype: "jsonb", wantnil: true},
		{src: []int(nil), dst: new([]int), pgtype: "jsonb", wantnil: true},
		{src: []int{}, dst: new([]int), pgtype: "jsonb", wantzero: true},
		{src: []int{1, 2, 3}, dst: new([]int), pgtype: "jsonb"},
		{src: IntSlice{1, 2, 3}, dst: new(IntSlice), pgtype: "jsonb"},

		{src: nil, dst: pg.Array([]int(nil)), pgtype: "int[]", wanterr: "pg: Scan(non-pointer []int)"},
		{src: pg.Array([]int(nil)), dst: pg.Array(new([]int)), pgtype: "int[]", wantnil: true},
		{src: pg.Array([]int{}), dst: pg.Array(new([]int)), pgtype: "int[]"},
		{src: pg.Array([]int{1, 2, 3}), dst: pg.Array(new([]int)), pgtype: "int[]"},

		{src: nil, dst: pg.Array([]int64(nil)), pgtype: "bigint[]", wanterr: "pg: Scan(non-pointer []int64)"},
		{src: nil, dst: pg.Array(new([]int64)), pgtype: "bigint[]", wantnil: true},
		{src: pg.Array([]int64(nil)), dst: pg.Array(new([]int64)), pgtype: "bigint[]", wantnil: true},
		{src: pg.Array([]int64{}), dst: pg.Array(new([]int64)), pgtype: "bigint[]"},
		{src: pg.Array([]int64{1, 2, 3}), dst: pg.Array(new([]int64)), pgtype: "bigint[]"},

		{src: nil, dst: pg.Array([]float64(nil)), pgtype: "decimal[]", wanterr: "pg: Scan(non-pointer []float64)"},
		{src: nil, dst: pg.Array(new([]float64)), pgtype: "decimal[]", wantnil: true},
		{src: pg.Array([]float64(nil)), dst: pg.Array(new([]float64)), pgtype: "decimal[]", wantnil: true},
		{src: pg.Array([]float64{}), dst: pg.Array(new([]float64)), pgtype: "decimal[]"},
		{src: pg.Array([]float64{1.1, 2.22, 3.333}), dst: pg.Array(new([]float64)), pgtype: "decimal[]"},

		{src: nil, dst: pg.Array([]string(nil)), pgtype: "text[]", wanterr: "pg: Scan(non-pointer []string)"},
		{src: nil, dst: pg.Array(new([]string)), pgtype: "text[]", wantnil: true},
		{src: pg.Array([]string(nil)), dst: pg.Array(new([]string)), pgtype: "text[]", wantnil: true},
		{src: pg.Array([]string{}), dst: pg.Array(new([]string)), pgtype: "text[]"},
		{src: pg.Array([]string{"one", "two", "three"}), dst: pg.Array(new([]string)), pgtype: "text[]"},
		{src: pg.Array([]string{`'"{}`}), dst: pg.Array(new([]string)), pgtype: "text[]"},

		{src: nil, dst: pg.Array([][]string(nil)), pgtype: "text[][]", wanterr: "pg: Scan(non-pointer [][]string)"},
		{src: nil, dst: pg.Array(new([][]string)), pgtype: "text[][]", wantnil: true},
		{src: pg.Array([][]string(nil)), dst: pg.Array(new([]string)), pgtype: "text[][]", wantnil: true},
		{src: pg.Array([][]string{}), dst: pg.Array(new([][]string)), pgtype: "text[][]"},
		{src: pg.Array([][]string{{"one", "two"}, {"three", "four"}}), dst: pg.Array(new([][]string)), pgtype: "text[][]"},
		{src: pg.Array([][]string{{`'"\{}`}}), dst: pg.Array(new([][]string)), pgtype: "text[][]"},

		{src: nil, dst: pg.Hstore(map[string]string(nil)), pgtype: "hstore", wanterr: "pg: Scan(non-pointer map[string]string)"},
		{src: nil, dst: pg.Hstore(new(map[string]string)), pgtype: "hstore", wantnil: true},
		{src: pg.Hstore(map[string]string(nil)), dst: pg.Hstore(new(map[string]string)), pgtype: "hstore", wantnil: true},
		{src: pg.Hstore(map[string]string{}), dst: pg.Hstore(new(map[string]string)), pgtype: "hstore"},
		{src: pg.Hstore(map[string]string{"foo": "bar"}), dst: pg.Hstore(new(map[string]string)), pgtype: "hstore"},
		{src: pg.Hstore(map[string]string{`'"\{}=>`: `'"\{}=>`}), dst: pg.Hstore(new(map[string]string)), pgtype: "hstore"},

		{src: nil, dst: sql.NullBool{}, pgtype: "bool", wanterr: "pg: Scan(non-pointer sql.NullBool)"},
		{src: nil, dst: new(*sql.NullBool), pgtype: "bool", wantnil: true},
		{src: nil, dst: new(sql.NullBool), pgtype: "bool", wanted: sql.NullBool{}},
		{src: &sql.NullBool{}, dst: new(sql.NullBool), pgtype: "bool"},
		{src: &sql.NullBool{Valid: true}, dst: new(sql.NullBool), pgtype: "bool"},
		{src: &sql.NullBool{Valid: true, Bool: true}, dst: new(sql.NullBool), pgtype: "bool"},

		{src: &sql.NullString{}, dst: new(sql.NullString), pgtype: "text"},
		{src: &sql.NullString{Valid: true}, dst: new(sql.NullString), pgtype: "text"},
		{src: &sql.NullString{Valid: true, String: "foo"}, dst: new(sql.NullString), pgtype: "text"},

		{src: &sql.NullInt64{}, dst: new(sql.NullInt64), pgtype: "bigint"},
		{src: &sql.NullInt64{Valid: true}, dst: new(sql.NullInt64), pgtype: "bigint"},
		{src: &sql.NullInt64{Valid: true, Int64: math.MaxInt64}, dst: new(sql.NullInt64), pgtype: "bigint"},

		{src: &sql.NullFloat64{}, dst: new(sql.NullFloat64), pgtype: "decimal"},
		{src: &sql.NullFloat64{Valid: true}, dst: new(sql.NullFloat64), pgtype: "decimal"},
		{src: &sql.NullFloat64{Valid: true, Float64: math.MaxFloat64}, dst: new(sql.NullFloat64), pgtype: "decimal"},

		{src: nil, dst: customStrSlice{}, wanterr: "pg: Scan(non-pointer pg_test.customStrSlice)"},
		{src: nil, dst: new(customStrSlice), wantnil: true},
		{src: nil, dst: new(*customStrSlice), wantnil: true},
		{src: customStrSlice{}, dst: new(customStrSlice), wantzero: true},
		{src: customStrSlice{"one", "two"}, dst: new(customStrSlice)},

		{src: nil, dst: time.Time{}, wanterr: "pg: Scan(non-pointer time.Time)"},
		{src: nil, dst: new(time.Time), pgtype: "timestamptz", wantzero: true},
		{src: nil, dst: new(*time.Time), pgtype: "timestamptz", wantnil: true},
		{src: time.Now(), dst: new(time.Time), pgtype: "timestamptz"},
		{src: time.Now(), dst: new(*time.Time), pgtype: "timestamptz"},
		{src: time.Now().UTC(), dst: new(time.Time), pgtype: "timestamptz"},
		{src: time.Time{}, dst: new(time.Time), pgtype: "timestamptz"},

		{src: nil, dst: pg.Array([]time.Time(nil)), pgtype: "timestamptz[]", wanterr: "pg: Scan(non-pointer []time.Time)"},
		{src: nil, dst: pg.Array(new([]time.Time)), pgtype: "timestamptz[]", wantnil: true},
		{src: pg.Array([]time.Time(nil)), dst: pg.Array(new([]time.Time)), pgtype: "timestamptz[]", wantnil: true},
		{src: pg.Array([]time.Time{}), dst: pg.Array(new([]time.Time)), pgtype: "timestamptz[]"},
		{src: pg.Array([]time.Time{time.Now(), time.Now(), time.Now()}), dst: pg.Array(new([]time.Time)), pgtype: "timestamptz[]"},

		{src: nil, dst: pg.Ints{}, wanterr: "pg: Scan(non-pointer pg.Ints)"},
		{src: 1, dst: new(pg.Ints), wanted: pg.Ints{1}},

		{src: nil, dst: pg.Strings{}, wanterr: "pg: Scan(non-pointer pg.Strings)"},
		{src: "hello", dst: new(pg.Strings), wanted: pg.Strings{"hello"}},

		{src: nil, dst: pg.IntSet{}, wanterr: "pg: Scan(non-pointer pg.IntSet)"},
		{src: 1, dst: new(pg.IntSet), wanted: pg.IntSet{1: struct{}{}}},

		{src: nil, dst: JSONMap{}, pgtype: "json", wanterr: "pg: Scan(non-pointer pg_test.JSONMap)"},
		{src: nil, dst: new(JSONMap), pgtype: "json", wantnil: true},
		{src: nil, dst: new(*JSONMap), pgtype: "json", wantnil: true},
		{src: JSONMap{}, dst: new(JSONMap), pgtype: "json"},
		{src: JSONMap{}, dst: new(*JSONMap), pgtype: "json"},
		{src: JSONMap{"foo": "bar"}, dst: new(JSONMap), pgtype: "json"},
		{src: `{"foo": "bar"}`, dst: new(JSONMap), pgtype: "json", wanted: JSONMap{"foo": "bar"}},

		{src: nil, dst: Struct{}, pgtype: "json", wanterr: "pg: Scan(non-pointer pg_test.Struct)"},
		{src: nil, dst: new(*Struct), pgtype: "json", wantnil: true},
		{src: nil, dst: new(Struct), pgtype: "json", wantzero: true},
		{src: Struct{}, dst: new(Struct), pgtype: "json"},
		{src: Struct{Foo: "bar"}, dst: new(Struct), pgtype: "json"},
		{src: `{"foo": "bar"}`, dst: new(Struct), wanted: Struct{Foo: "bar"}},

		{src: nil, dst: new(net.IP), wanted: net.IP(nil), pgtype: "inet"},
		{src: net.ParseIP("127.0.0.1"), dst: new(net.IP), pgtype: "inet"},
		{src: net.ParseIP("::10.2.3.4"), dst: new(net.IP), pgtype: "inet"},
		{src: net.ParseIP("::ffff:10.4.3.2"), dst: new(net.IP), pgtype: "inet"},

		{src: nil, dst: (*net.IPNet)(nil), pgtype: "cidr", wanterr: "pg: Scan(non-pointer *net.IPNet)"},
		{src: nil, dst: new(net.IPNet), wanted: net.IPNet{}, pgtype: "cidr"},
		{src: nil, dst: mustParseCIDR("192.168.100.128/25"), wanted: net.IPNet{}, pgtype: "cidr"},
		{src: mustParseCIDR("192.168.100.128/25"), dst: new(net.IPNet), pgtype: "cidr"},
		{src: mustParseCIDR("2001:4f8:3:ba::/64"), dst: new(net.IPNet), pgtype: "cidr"},
		{src: mustParseCIDR("2001:4f8:3:ba:2e0:81ff:fe22:d1f1/128"), dst: new(net.IPNet), pgtype: "cidr"},
	}
}

func TestConversion(t *testing.T) {
	db := pg.Connect(pgOptions())

	for i, test := range conversionTests() {
		test.i = i

		var scanner orm.ColumnScanner
		if v, ok := test.dst.(orm.ColumnScanner); ok {
			scanner = v
		} else {
			scanner = pg.Scan(test.dst)
		}

		_, err := db.QueryOne(scanner, "SELECT (?) AS dst", test.src)
		test.Assert(t, err)
	}

	for i, test := range conversionTests() {
		test.i = i

		var scanner orm.ColumnScanner
		if v, ok := test.dst.(orm.ColumnScanner); ok {
			scanner = v
		} else {
			scanner = pg.Scan(test.dst)
		}

		err := db.Model().ColumnExpr("(?) AS dst", test.src).Select(scanner)
		test.Assert(t, err)
	}

	for i, test := range conversionTests() {
		test.i = i

		if test.pgtype == "" {
			continue
		}

		stmt, err := db.Prepare(fmt.Sprintf("SELECT ($1::%s) AS dst", test.pgtype))
		if err != nil {
			t.Fatal(err)
		}

		var scanner orm.ColumnScanner
		if v, ok := test.dst.(orm.ColumnScanner); ok {
			scanner = v
		} else {
			scanner = pg.Scan(test.dst)
		}

		_, err = stmt.QueryOne(scanner, test.src)
		test.Assert(t, err)

		if err := stmt.Close(); err != nil {
			t.Fatal(err)
		}
	}
}

func mustParseCIDR(s string) *net.IPNet {
	_, ipnet, err := net.ParseCIDR(s)
	if err != nil {
		panic(err)
	}
	return ipnet
}
