package pgtype_test

import (
	"context"
	"fmt"
	"testing"

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

func TestCompositeCodecTranscode(t *testing.T) {
	defaultConnTestRunner.RunTest(context.Background(), t, func(ctx context.Context, t testing.TB, conn *pgx.Conn) {
		_, err := conn.Exec(ctx, `drop type if exists ct_test;

create type ct_test as (
	a text,
  b int4
);`)
		require.NoError(t, err)
		defer conn.Exec(ctx, "drop type ct_test")

		dt, err := conn.LoadType(ctx, "ct_test")
		require.NoError(t, err)
		conn.TypeMap().RegisterType(dt)

		formats := []struct {
			name string
			code int16
		}{
			{name: "TextFormat", code: pgx.TextFormatCode},
			{name: "BinaryFormat", code: pgx.BinaryFormatCode},
		}

		for _, format := range formats {
			var a string
			var b int32

			err := conn.QueryRow(ctx, "select $1::ct_test", pgx.QueryResultFormats{format.code},
				pgtype.CompositeFields{"hi", int32(42)},
			).Scan(
				pgtype.CompositeFields{&a, &b},
			)
			require.NoErrorf(t, err, "%v", format.name)
			require.EqualValuesf(t, "hi", a, "%v", format.name)
			require.EqualValuesf(t, 42, b, "%v", format.name)
		}
	})
}

// stringerInt32 is a custom int32 type that implements fmt.Stringer, similar to protobuf enums.
type stringerInt32 int32

const (
	stringerInt32Foo stringerInt32 = 100
	stringerInt32Bar stringerInt32 = 200
)

func (s stringerInt32) String() string {
	switch s {
	case stringerInt32Foo:
		return "FOO"
	case stringerInt32Bar:
		return "BAR"
	default:
		return "UNKNOWN"
	}
}

// https://github.com/jackc/pgx/discussions/2527
func TestCompositeCodecTranscodeWithStringerInt(t *testing.T) {
	defaultConnTestRunner.RunTest(context.Background(), t, func(ctx context.Context, t testing.TB, conn *pgx.Conn) {
		_, err := conn.Exec(ctx, `drop type if exists ct_stringer_test;

create type ct_stringer_test as (
	a text,
	b smallint
);`)
		require.NoError(t, err)
		defer conn.Exec(ctx, "drop type ct_stringer_test")

		dt, err := conn.LoadType(ctx, "ct_stringer_test")
		require.NoError(t, err)
		conn.TypeMap().RegisterType(dt)

		formats := []struct {
			name string
			code int16
		}{
			{name: "TextFormat", code: pgx.TextFormatCode},
			{name: "BinaryFormat", code: pgx.BinaryFormatCode},
		}

		// Pass the same stringerInt32 value for both fields. The text field should get the Stringer value
		// ("BAR") and the smallint field should get the integer value (200).
		for _, format := range formats {
			var a string
			var b int16

			err := conn.QueryRow(ctx, "select $1::ct_stringer_test", pgx.QueryResultFormats{format.code},
				pgtype.CompositeFields{stringerInt32Bar, stringerInt32Bar},
			).Scan(
				pgtype.CompositeFields{&a, &b},
			)
			require.NoErrorf(t, err, "%v", format.name)
			require.EqualValuesf(t, "BAR", a, "%v", format.name)
			require.EqualValuesf(t, 200, b, "%v", format.name)
		}
	})
}

type point3d struct {
	X, Y, Z float64
}

func (p point3d) IsNull() bool {
	return false
}

func (p point3d) Index(i int) any {
	switch i {
	case 0:
		return p.X
	case 1:
		return p.Y
	case 2:
		return p.Z
	default:
		panic("invalid index")
	}
}

func (p *point3d) ScanNull() error {
	return fmt.Errorf("cannot scan NULL into point3d")
}

func (p *point3d) ScanIndex(i int) any {
	switch i {
	case 0:
		return &p.X
	case 1:
		return &p.Y
	case 2:
		return &p.Z
	default:
		panic("invalid index")
	}
}

func TestCompositeCodecTranscodeStruct(t *testing.T) {
	defaultConnTestRunner.RunTest(context.Background(), t, func(ctx context.Context, t testing.TB, conn *pgx.Conn) {
		_, err := conn.Exec(ctx, `drop type if exists point3d;

create type point3d as (
	x float8,
	y float8,
	z float8
);`)
		require.NoError(t, err)
		defer conn.Exec(ctx, "drop type point3d")

		dt, err := conn.LoadType(ctx, "point3d")
		require.NoError(t, err)
		conn.TypeMap().RegisterType(dt)

		formats := []struct {
			name string
			code int16
		}{
			{name: "TextFormat", code: pgx.TextFormatCode},
			{name: "BinaryFormat", code: pgx.BinaryFormatCode},
		}

		for _, format := range formats {
			input := point3d{X: 1, Y: 2, Z: 3}
			var output point3d
			err := conn.QueryRow(ctx, "select $1::point3d", pgx.QueryResultFormats{format.code}, input).Scan(&output)
			require.NoErrorf(t, err, "%v", format.name)
			require.Equalf(t, input, output, "%v", format.name)
		}
	})
}

func TestCompositeCodecTranscodeStructWrapper(t *testing.T) {
	defaultConnTestRunner.RunTest(context.Background(), t, func(ctx context.Context, t testing.TB, conn *pgx.Conn) {
		_, err := conn.Exec(ctx, `drop type if exists point3d;

create type point3d as (
	x float8,
	y float8,
	z float8
);`)
		require.NoError(t, err)
		defer conn.Exec(ctx, "drop type point3d")

		dt, err := conn.LoadType(ctx, "point3d")
		require.NoError(t, err)
		conn.TypeMap().RegisterType(dt)

		formats := []struct {
			name string
			code int16
		}{
			{name: "TextFormat", code: pgx.TextFormatCode},
			{name: "BinaryFormat", code: pgx.BinaryFormatCode},
		}

		type anotherPoint struct {
			X, Y, Z float64
		}

		for _, format := range formats {
			input := anotherPoint{X: 1, Y: 2, Z: 3}
			var output anotherPoint
			err := conn.QueryRow(ctx, "select $1::point3d", pgx.QueryResultFormats{format.code}, input).Scan(&output)
			require.NoErrorf(t, err, "%v", format.name)
			require.Equalf(t, input, output, "%v", format.name)
		}
	})
}

type parent struct {
	Name     string
	Location *point3d
}

func (p parent) IsNull() bool {
	return false
}

func (p parent) Index(i int) any {
	switch i {
	case 0:
		return p.Name
	case 1:
		return p.Location
	default:
		panic("invalid index")
	}
}

func (p *parent) ScanNull() error {
	return fmt.Errorf("cannot scan NULL into parent")
}

func (p *parent) ScanIndex(i int) any {
	switch i {
	case 0:
		return &p.Name
	case 1:
		return &p.Location
	default:
		panic("invalid index")
	}
}

// https://github.com/jackc/pgx/issues/2453
func TestCompositeCodecTranscodeStructWithNilPointer(t *testing.T) {
	defaultConnTestRunner.RunTest(context.Background(), t, func(ctx context.Context, t testing.TB, conn *pgx.Conn) {
		skipCockroachDB(t, "Server does not support nested composite types")

		_, err := conn.Exec(ctx, `drop type if exists parent;
drop type if exists point3d;

create type point3d as (
	x float8,
	y float8,
	z float8
);

create type parent as (
	name text,
	location point3d
);`)
		require.NoError(t, err)
		defer conn.Exec(ctx, "drop type parent")
		defer conn.Exec(ctx, "drop type point3d")

		dataTypes, err := conn.LoadTypes(ctx, []string{"point3d", "parent"})
		require.NoError(t, err)
		conn.TypeMap().RegisterTypes(dataTypes)

		formats := []struct {
			name string
			code int16
		}{
			{name: "TextFormat", code: pgx.TextFormatCode},
			{name: "BinaryFormat", code: pgx.BinaryFormatCode},
		}

		for _, format := range formats {
			input := parent{Name: "test", Location: nil}
			var output parent
			err := conn.QueryRow(ctx, "select $1::parent", pgx.QueryResultFormats{format.code}, input).Scan(&output)
			require.NoErrorf(t, err, "%v", format.name)
			require.Equalf(t, input, output, "%v", format.name)
		}
	})
}

type parentWithSlice struct {
	Name      string
	Locations []*point3d
}

func (p parentWithSlice) IsNull() bool {
	return false
}

func (p parentWithSlice) Index(i int) any {
	switch i {
	case 0:
		return p.Name
	case 1:
		return p.Locations
	default:
		panic("invalid index")
	}
}

func (p *parentWithSlice) ScanNull() error {
	return fmt.Errorf("cannot scan NULL into parent")
}

func (p *parentWithSlice) ScanIndex(i int) any {
	switch i {
	case 0:
		return &p.Name
	case 1:
		return &p.Locations
	default:
		panic("invalid index")
	}
}

// https://github.com/jackc/pgx/issues/2453
func TestCompositeCodecTranscodeStructWithSliceOfNilPointer(t *testing.T) {
	defaultConnTestRunner.RunTest(context.Background(), t, func(ctx context.Context, t testing.TB, conn *pgx.Conn) {
		skipCockroachDB(t, "Server does not support nested composite types")

		_, err := conn.Exec(ctx, `drop type if exists parent;
drop type if exists point3d;

create type point3d as (
	x float8,
	y float8,
	z float8
);

create type parent_with_slice as (
	name text,
	locations point3d[]
);`)
		require.NoError(t, err)
		defer conn.Exec(ctx, "drop type parent_with_slice")
		defer conn.Exec(ctx, "drop type point3d")

		dataTypes, err := conn.LoadTypes(ctx, []string{"point3d", "parent_with_slice"})
		require.NoError(t, err)
		conn.TypeMap().RegisterTypes(dataTypes)

		formats := []struct {
			name string
			code int16
		}{
			{name: "TextFormat", code: pgx.TextFormatCode},
			{name: "BinaryFormat", code: pgx.BinaryFormatCode},
		}

		for _, format := range formats {
			input := parentWithSlice{Name: "test", Locations: []*point3d{nil}}
			var output parentWithSlice
			err := conn.QueryRow(ctx, "select $1::parent_with_slice", pgx.QueryResultFormats{format.code}, input).Scan(&output)
			require.NoErrorf(t, err, "%v", format.name)
			require.Equalf(t, input, output, "%v", format.name)
		}
	})
}

func TestCompositeCodecDecodeValue(t *testing.T) {
	defaultConnTestRunner.RunTest(context.Background(), t, func(ctx context.Context, t testing.TB, conn *pgx.Conn) {
		_, err := conn.Exec(ctx, `drop type if exists point3d;

create type point3d as (
	x float8,
	y float8,
	z float8
);`)
		require.NoError(t, err)
		defer conn.Exec(ctx, "drop type point3d")

		dt, err := conn.LoadType(ctx, "point3d")
		require.NoError(t, err)
		conn.TypeMap().RegisterType(dt)

		formats := []struct {
			name string
			code int16
		}{
			{name: "TextFormat", code: pgx.TextFormatCode},
			{name: "BinaryFormat", code: pgx.BinaryFormatCode},
		}

		for _, format := range formats {
			rows, err := conn.Query(ctx, "select '(1,2,3)'::point3d", pgx.QueryResultFormats{format.code})
			require.NoErrorf(t, err, "%v", format.name)
			require.True(t, rows.Next())
			values, err := rows.Values()
			require.NoErrorf(t, err, "%v", format.name)
			require.Lenf(t, values, 1, "%v", format.name)
			require.Equalf(t, map[string]any{"x": 1.0, "y": 2.0, "z": 3.0}, values[0], "%v", format.name)
			require.False(t, rows.Next())
			require.NoErrorf(t, rows.Err(), "%v", format.name)
		}
	})
}

// Test for composite type from table instead of create type. Table types have system / hidden columns like tableoid,
// cmax, xmax, etc. These are not included when sending or receiving composite types.
//
// https://github.com/jackc/pgx/issues/1576
func TestCompositeCodecTranscodeStructWrapperForTable(t *testing.T) {
	skipCockroachDB(t, "Server does not support composite types from table definitions")

	defaultConnTestRunner.RunTest(context.Background(), t, func(ctx context.Context, t testing.TB, conn *pgx.Conn) {
		_, err := conn.Exec(ctx, `drop table if exists point3d;

create table point3d (
	x float8,
	y float8,
	z float8
);`)
		require.NoError(t, err)
		defer conn.Exec(ctx, "drop table point3d")

		dt, err := conn.LoadType(ctx, "point3d")
		require.NoError(t, err)
		conn.TypeMap().RegisterType(dt)

		formats := []struct {
			name string
			code int16
		}{
			{name: "TextFormat", code: pgx.TextFormatCode},
			{name: "BinaryFormat", code: pgx.BinaryFormatCode},
		}

		type anotherPoint struct {
			X, Y, Z float64
		}

		for _, format := range formats {
			input := anotherPoint{X: 1, Y: 2, Z: 3}
			var output anotherPoint
			err := conn.QueryRow(ctx, "select $1::point3d", pgx.QueryResultFormats{format.code}, input).Scan(&output)
			require.NoErrorf(t, err, "%v", format.name)
			require.Equalf(t, input, output, "%v", format.name)
		}
	})
}
