package pgtype_test

import (
	"context"
	"database/sql"
	"database/sql/driver"
	"encoding/json"
	"errors"
	"fmt"
	"reflect"
	"testing"

	pgx "github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgtype"
	"github.com/jackc/pgx/v5/pgxtest"
	"github.com/stretchr/testify/require"
)

func isExpectedEqMap(a any) func(any) bool {
	return func(v any) bool {
		aa := a.(map[string]any)
		bb := v.(map[string]any)

		if (aa == nil) != (bb == nil) {
			return false
		}

		if aa == nil {
			return true
		}

		if len(aa) != len(bb) {
			return false
		}

		for k := range aa {
			if aa[k] != bb[k] {
				return false
			}
		}

		return true
	}
}

func TestJSONCodec(t *testing.T) {
	type jsonStruct struct {
		Name string `json:"name"`
		Age  int    `json:"age"`
	}

	var str string
	pgxtest.RunValueRoundTripTests(context.Background(), t, defaultConnTestRunner, nil, "json", []pgxtest.ValueRoundTripTest{
		{Param: nil, Result: new(*jsonStruct), Test: isExpectedEq((*jsonStruct)(nil))},
		{Param: map[string]any(nil), Result: new(*string), Test: isExpectedEq((*string)(nil))},
		{Param: map[string]any(nil), Result: new([]byte), Test: isExpectedEqBytes([]byte(nil))},
		{Param: []byte(nil), Result: new([]byte), Test: isExpectedEqBytes([]byte(nil))},
		{Param: nil, Result: new([]byte), Test: isExpectedEqBytes([]byte(nil))},

		// Test sql.Scanner. (https://github.com/jackc/pgx/issues/1418)
		{Param: "42", Result: new(sql.NullInt64), Test: isExpectedEq(sql.NullInt64{Int64: 42, Valid: true})},

		// Test driver.Valuer. (https://github.com/jackc/pgx/issues/1430)
		{Param: sql.NullInt64{Int64: 42, Valid: true}, Result: new(sql.NullInt64), Test: isExpectedEq(sql.NullInt64{Int64: 42, Valid: true})},

		// Test driver.Valuer is used before json.Marshaler (https://github.com/jackc/pgx/issues/1805)
		{Param: Issue1805(7), Result: new(Issue1805), Test: isExpectedEq(Issue1805(7))},
		// Test driver.Scanner is used before json.Unmarshaler (https://github.com/jackc/pgx/issues/2146)
		{Param: Issue2146(7), Result: new(*Issue2146), Test: isPtrExpectedEq(Issue2146(7))},

		// Test driver.Scanner without pointer receiver (https://github.com/jackc/pgx/issues/2204)
		{Param: NonPointerJSONScanner{V: stringPtr("{}")}, Result: NonPointerJSONScanner{V: &str}, Test: func(a any) bool { return str == "{}" }},
	})

	pgxtest.RunValueRoundTripTests(context.Background(), t, defaultConnTestRunner, pgxtest.KnownOIDQueryExecModes, "json", []pgxtest.ValueRoundTripTest{
		{Param: []byte("{}"), Result: new([]byte), Test: isExpectedEqBytes([]byte("{}"))},
		{Param: []byte("null"), Result: new([]byte), Test: isExpectedEqBytes([]byte("null"))},
		{Param: []byte("42"), Result: new([]byte), Test: isExpectedEqBytes([]byte("42"))},
		{Param: []byte(`"hello"`), Result: new([]byte), Test: isExpectedEqBytes([]byte(`"hello"`))},
		{Param: []byte(`"hello"`), Result: new(string), Test: isExpectedEq(`"hello"`)},
		{Param: map[string]any{"foo": "bar"}, Result: new(map[string]any), Test: isExpectedEqMap(map[string]any{"foo": "bar"})},
		{Param: jsonStruct{Name: "Adam", Age: 10}, Result: new(jsonStruct), Test: isExpectedEq(jsonStruct{Name: "Adam", Age: 10})},
	})
}

type Issue1805 int

func (i *Issue1805) Scan(src any) error {
	var source []byte
	switch src.(type) {
	case string:
		source = []byte(src.(string))
	case []byte:
		source = src.([]byte)
	default:
		return errors.New("unknown source type")
	}
	var newI int
	if err := json.Unmarshal(source, &newI); err != nil {
		return err
	}
	*i = Issue1805(newI)
	return nil
}

func (i Issue1805) Value() (driver.Value, error) {
	b, err := json.Marshal(int(i))
	return string(b), err
}

func (i Issue1805) UnmarshalJSON(bytes []byte) error {
	return errors.New("UnmarshalJSON called")
}

func (i Issue1805) MarshalJSON() ([]byte, error) {
	return nil, errors.New("MarshalJSON called")
}

type Issue2146 int

func (i *Issue2146) Scan(src any) error {
	var source []byte
	switch src.(type) {
	case string:
		source = []byte(src.(string))
	case []byte:
		source = src.([]byte)
	default:
		return errors.New("unknown source type")
	}
	var newI int
	if err := json.Unmarshal(source, &newI); err != nil {
		return err
	}
	*i = Issue2146(newI + 1)
	return nil
}

func (i Issue2146) Value() (driver.Value, error) {
	b, err := json.Marshal(int(i - 1))
	return string(b), err
}

type NonPointerJSONScanner struct {
	V *string
}

func (i NonPointerJSONScanner) Scan(src any) error {
	switch c := src.(type) {
	case string:
		*i.V = c
	case []byte:
		*i.V = string(c)
	default:
		return errors.New("unknown source type")
	}

	return nil
}

func (i NonPointerJSONScanner) Value() (driver.Value, error) {
	return i.V, nil
}

// https://github.com/jackc/pgx/issues/1273#issuecomment-1221414648
func TestJSONCodecUnmarshalSQLNull(t *testing.T) {
	defaultConnTestRunner.RunTest(context.Background(), t, func(ctx context.Context, t testing.TB, conn *pgx.Conn) {
		// Slices are nilified
		slice := []string{"foo", "bar", "baz"}
		err := conn.QueryRow(ctx, "select null::json").Scan(&slice)
		require.NoError(t, err)
		require.Nil(t, slice)

		// Maps are nilified
		m := map[string]any{"foo": "bar"}
		err = conn.QueryRow(ctx, "select null::json").Scan(&m)
		require.NoError(t, err)
		require.Nil(t, m)

		m = map[string]any{"foo": "bar"}
		err = conn.QueryRow(ctx, "select null::json").Scan(&m)
		require.NoError(t, err)
		require.Nil(t, m)

		// Pointer to pointer are nilified
		n := 42
		p := &n
		err = conn.QueryRow(ctx, "select null::json").Scan(&p)
		require.NoError(t, err)
		require.Nil(t, p)

		// A string cannot scan a NULL.
		str := "foobar"
		err = conn.QueryRow(ctx, "select null::json").Scan(&str)
		fieldName := "json"
		if conn.PgConn().ParameterStatus("crdb_version") != "" {
			fieldName = "jsonb" // Seems like CockroachDB treats json as jsonb.
		}
		require.EqualError(t, err, fmt.Sprintf("can't scan into dest[0] (col: %s): cannot scan NULL into *string", fieldName))

		// A non-string cannot scan a NULL.
		err = conn.QueryRow(ctx, "select null::json").Scan(&n)
		require.EqualError(t, err, fmt.Sprintf("can't scan into dest[0] (col: %s): cannot scan NULL into *int", fieldName))
	})
}

// https://github.com/jackc/pgx/issues/1470
func TestJSONCodecPointerToPointerToString(t *testing.T) {
	defaultConnTestRunner.RunTest(context.Background(), t, func(ctx context.Context, t testing.TB, conn *pgx.Conn) {
		var s *string
		err := conn.QueryRow(ctx, "select '{}'::json").Scan(&s)
		require.NoError(t, err)
		require.NotNil(t, s)
		require.Equal(t, "{}", *s)

		err = conn.QueryRow(ctx, "select null::json").Scan(&s)
		require.NoError(t, err)
		require.Nil(t, s)
	})
}

// https://github.com/jackc/pgx/issues/1691
func TestJSONCodecPointerToPointerToInt(t *testing.T) {
	defaultConnTestRunner.RunTest(context.Background(), t, func(ctx context.Context, t testing.TB, conn *pgx.Conn) {
		n := 44
		p := &n
		err := conn.QueryRow(ctx, "select 'null'::jsonb").Scan(&p)
		require.NoError(t, err)
		require.Nil(t, p)
	})
}

// https://github.com/jackc/pgx/issues/1691
func TestJSONCodecPointerToPointerToStruct(t *testing.T) {
	defaultConnTestRunner.RunTest(context.Background(), t, func(ctx context.Context, t testing.TB, conn *pgx.Conn) {
		type ImageSize struct {
			Height int    `json:"height"`
			Width  int    `json:"width"`
			Str    string `json:"str"`
		}
		is := &ImageSize{Height: 100, Width: 100, Str: "str"}
		err := conn.QueryRow(ctx, `select 'null'::jsonb`).Scan(&is)
		require.NoError(t, err)
		require.Nil(t, is)
	})
}

func TestJSONCodecClearExistingValueBeforeUnmarshal(t *testing.T) {
	defaultConnTestRunner.RunTest(context.Background(), t, func(ctx context.Context, t testing.TB, conn *pgx.Conn) {
		m := map[string]any{}
		err := conn.QueryRow(ctx, `select '{"foo": "bar"}'::json`).Scan(&m)
		require.NoError(t, err)
		require.Equal(t, map[string]any{"foo": "bar"}, m)

		err = conn.QueryRow(ctx, `select '{"baz": "quz"}'::json`).Scan(&m)
		require.NoError(t, err)
		require.Equal(t, map[string]any{"baz": "quz"}, m)
	})
}

type ParentIssue1681 struct {
	Child ChildIssue1681
}

func (t *ParentIssue1681) MarshalJSON() ([]byte, error) {
	return []byte(`{"custom":"thing"}`), nil
}

type ChildIssue1681 struct{}

func (t ChildIssue1681) MarshalJSON() ([]byte, error) {
	return []byte(`{"someVal": false}`), nil
}

// https://github.com/jackc/pgx/issues/1681
func TestJSONCodecEncodeJSONMarshalerThatCanBeWrapped(t *testing.T) {
	skipCockroachDB(t, "CockroachDB treats json as jsonb. This causes it to format differently than PostgreSQL.")

	defaultConnTestRunner.RunTest(context.Background(), t, func(ctx context.Context, t testing.TB, conn *pgx.Conn) {
		var jsonStr string
		err := conn.QueryRow(context.Background(), "select $1::json", &ParentIssue1681{}).Scan(&jsonStr)
		require.NoError(t, err)
		require.Equal(t, `{"custom":"thing"}`, jsonStr)
	})
}

func TestJSONCodecCustomMarshal(t *testing.T) {
	skipCockroachDB(t, "CockroachDB treats json as jsonb. This causes it to format differently than PostgreSQL.")

	connTestRunner := defaultConnTestRunner
	connTestRunner.AfterConnect = func(ctx context.Context, t testing.TB, conn *pgx.Conn) {
		conn.TypeMap().RegisterType(&pgtype.Type{
			Name: "json", OID: pgtype.JSONOID, Codec: &pgtype.JSONCodec{
				Marshal: func(v any) ([]byte, error) {
					return []byte(`{"custom":"value"}`), nil
				},
				Unmarshal: func(data []byte, v any) error {
					return json.Unmarshal([]byte(`{"custom":"value"}`), v)
				},
			},
		})
	}

	pgxtest.RunValueRoundTripTests(context.Background(), t, connTestRunner, pgxtest.KnownOIDQueryExecModes, "json", []pgxtest.ValueRoundTripTest{
		// There is no space between "custom" and "value" in json type.
		{Param: map[string]any{"something": "else"}, Result: new(string), Test: isExpectedEq(`{"custom":"value"}`)},
		{Param: []byte(`{"something":"else"}`), Result: new(map[string]any), Test: func(v any) bool {
			return reflect.DeepEqual(v, map[string]any{"custom": "value"})
		}},
	})
}

func TestJSONCodecScanToNonPointerValues(t *testing.T) {
	defaultConnTestRunner.RunTest(context.Background(), t, func(ctx context.Context, t testing.TB, conn *pgx.Conn) {
		n := 44
		err := conn.QueryRow(ctx, "select '42'::jsonb").Scan(n)
		require.Error(t, err)

		var i *int
		err = conn.QueryRow(ctx, "select '42'::jsonb").Scan(i)
		require.Error(t, err)

		m := 0
		err = conn.QueryRow(ctx, "select '42'::jsonb").Scan(&m)
		require.NoError(t, err)
		require.Equal(t, 42, m)
	})
}

func TestJSONCodecScanNull(t *testing.T) {
	defaultConnTestRunner.RunTest(context.Background(), t, func(ctx context.Context, t testing.TB, conn *pgx.Conn) {
		var dest struct{}
		err := conn.QueryRow(ctx, "select null::jsonb").Scan(&dest)
		require.Error(t, err)
		require.Contains(t, err.Error(), "cannot scan NULL into *struct {}")

		err = conn.QueryRow(ctx, "select 'null'::jsonb").Scan(&dest)
		require.NoError(t, err)

		var destPointer *struct{}
		err = conn.QueryRow(ctx, "select null::jsonb").Scan(&destPointer)
		require.NoError(t, err)
		require.Nil(t, destPointer)

		err = conn.QueryRow(ctx, "select 'null'::jsonb").Scan(&destPointer)
		require.NoError(t, err)
		require.Nil(t, destPointer)

		var raw json.RawMessage
		require.NoError(t, conn.QueryRow(ctx, "select 'null'::jsonb").Scan(&raw))
		require.Equal(t, json.RawMessage("null"), raw)
	})
}

func TestJSONCodecScanNullToPointerToSQLScanner(t *testing.T) {
	defaultConnTestRunner.RunTest(context.Background(), t, func(ctx context.Context, t testing.TB, conn *pgx.Conn) {
		var dest *Issue2146
		err := conn.QueryRow(ctx, "select null::jsonb").Scan(&dest)
		require.NoError(t, err)
		require.Nil(t, dest)
	})
}

type SQLScannerFunc func(src any) error

func (f SQLScannerFunc) Scan(src any) error {
	return f(src)
}

// https://github.com/jackc/pgx/issues/2403#issuecomment-3480454865
//
// Previously, when scanning a json/jsonb value into a sql.Scanner via database/sql the source value was a []byte.
// However, when scanning into a sql.Scanner via pgx the source value was a string. This test ensures that the source
// value is a []byte.
func TestJSONCodecSQLScannerReceivesByteSlice(t *testing.T) {
	defaultConnTestRunner.RunTest(context.Background(), t, func(ctx context.Context, t testing.TB, conn *pgx.Conn) {
		f := SQLScannerFunc(func(src any) error {
			require.IsType(t, []byte{}, src)
			return nil
		})
		err := conn.QueryRow(ctx, "select '{}'::json").Scan(&f)
		require.NoError(t, err)
	})
}
