// Copyright 2026 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: GPL-3.0-or-later

package db_test

import (
	"testing"

	"forgejo.org/models/db"
	"forgejo.org/models/unittest"
	"forgejo.org/modules/optional"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"xorm.io/xorm/schemas"
)

func TestOptionFieldInt(t *testing.T) {
	require.NoError(t, unittest.PrepareTestDatabase())

	type OptionInt struct {
		ID     int64 `xorm:"pk autoincr"`
		Number optional.Option[int64]
	}
	db.GetEngine(t.Context()).Sync(&OptionInt{})

	t.Run("insert null, read back", func(t *testing.T) {
		null := OptionInt{
			Number: optional.None[int64](),
		}
		cnt, err := db.GetEngine(t.Context()).Insert(&null)
		require.NoError(t, err)
		assert.EqualValues(t, 1, cnt)

		{
			var read OptionInt
			has, err := db.GetEngine(t.Context()).ID(null.ID).Get(&read)
			require.NoError(t, err)
			require.True(t, has)
			hasNumber, _ := read.Number.Get()
			assert.False(t, hasNumber)
		}
	})

	t.Run("insert not null, read back", func(t *testing.T) {
		notNull := OptionInt{
			Number: optional.Some[int64](123),
		}

		cnt, err := db.GetEngine(t.Context()).Insert(&notNull)
		require.NoError(t, err)
		assert.EqualValues(t, 1, cnt)
		{
			var read OptionInt
			has, err := db.GetEngine(t.Context()).ID(notNull.ID).Get(&read)
			require.NoError(t, err)
			require.True(t, has)
			hasNumber, number := read.Number.Get()
			assert.True(t, hasNumber)
			assert.EqualValues(t, 123, number)
		}
	})

	t.Run("read multiple records without filters", func(t *testing.T) {
		var arr []OptionInt
		err := db.GetEngine(t.Context()).Find(&arr)
		require.NoError(t, err)
		assert.Len(t, arr, 2)
	})

	t.Run("read multiple records with bean filters", func(t *testing.T) {
		var arr []OptionInt
		cond := &OptionInt{
			Number: optional.Some[int64](123),
		}
		err := db.GetEngine(t.Context()).Find(&arr, cond)
		require.NoError(t, err)
		require.Len(t, arr, 1)
		v := arr[0]
		has, value := v.Number.Get()
		assert.True(t, has)
		assert.EqualValues(t, 123, value)
	})
}

func TestOptionFieldString(t *testing.T) {
	require.NoError(t, unittest.PrepareTestDatabase())

	type OptionString struct {
		ID     int64 `xorm:"pk autoincr"`
		String optional.Option[string]
	}
	assert.NoError(t, db.GetEngine(t.Context()).Sync(new(OptionString)))

	t.Run("insert null, read back", func(t *testing.T) {
		null := OptionString{
			String: optional.None[string](),
		}
		cnt, err := db.GetEngine(t.Context()).Insert(&null)
		require.NoError(t, err)
		assert.EqualValues(t, 1, cnt)

		{
			var read OptionString
			has, err := db.GetEngine(t.Context()).ID(null.ID).Get(&read)
			require.NoError(t, err)
			require.True(t, has)
			hasString, _ := read.String.Get()
			assert.False(t, hasString)
		}
	})

	t.Run("insert not null, read back", func(t *testing.T) {
		notNull := OptionString{
			String: optional.Some("hello"),
		}

		cnt, err := db.GetEngine(t.Context()).Insert(&notNull)
		require.NoError(t, err)
		assert.EqualValues(t, 1, cnt)
		{
			var read OptionString
			has, err := db.GetEngine(t.Context()).ID(notNull.ID).Get(&read)
			require.NoError(t, err)
			require.True(t, has)
			hasString, str := read.String.Get()
			assert.True(t, hasString)
			assert.Equal(t, "hello", str)
		}
	})

	t.Run("read multiple records without filters", func(t *testing.T) {
		var arr []OptionString
		err := db.GetEngine(t.Context()).Find(&arr)
		require.NoError(t, err)
		assert.Len(t, arr, 2)
	})

	t.Run("read multiple records with bean filters", func(t *testing.T) {
		var arr []OptionString
		cond := &OptionString{
			String: optional.Some("hello"),
		}
		err := db.GetEngine(t.Context()).Find(&arr, cond)
		require.NoError(t, err)
		require.Len(t, arr, 1)
		v := arr[0]
		has, value := v.String.Get()
		assert.True(t, has)
		assert.Equal(t, "hello", value)
	})
}

func TestOptionFieldIntrospection(t *testing.T) {
	require.NoError(t, unittest.PrepareTestDatabase())

	type OptionIntrospectInt struct {
		ID           int64 `xorm:"pk autoincr"`
		OptionNumber optional.Option[int64]
		NormalNumber int64
	}
	assert.NoError(t, db.GetEngine(t.Context()).Sync(new(OptionIntrospectInt)))

	schema, err := db.GetEngine(t.Context()).NoAutoTime().Engine().TableInfo(&OptionIntrospectInt{})
	require.NoError(t, err)

	var optionColumn, normalColumn *schemas.Column
	for _, c := range schema.Columns() {
		switch c.Name {
		case "option_number":
			optionColumn = c
		case "normal_number":
			normalColumn = c
		}
	}
	require.NotNil(t, optionColumn)
	require.NotNil(t, normalColumn)

	assert.Equal(t, normalColumn.TableName, optionColumn.TableName, "field TableName")
	assert.Equal(t, normalColumn.SQLType, optionColumn.SQLType, "field SQLType")
	assert.Equal(t, normalColumn.IsJSON, optionColumn.IsJSON, "field IsJSON")
	assert.Equal(t, normalColumn.Length, optionColumn.Length, "field Length")
	assert.Equal(t, normalColumn.Length2, optionColumn.Length2, "field Length2")
	assert.Equal(t, normalColumn.Nullable, optionColumn.Nullable, "field Nullable")
	assert.Equal(t, normalColumn.Default, optionColumn.Default, "field Default")
	assert.Equal(t, normalColumn.Indexes, optionColumn.Indexes, "field Indexes")
	assert.Equal(t, normalColumn.IsPrimaryKey, optionColumn.IsPrimaryKey, "field IsPrimaryKey")
	assert.Equal(t, normalColumn.IsAutoIncrement, optionColumn.IsAutoIncrement, "field IsAutoIncrement")
	assert.Equal(t, normalColumn.MapType, optionColumn.MapType, "field MapType")
	assert.Equal(t, normalColumn.IsCreated, optionColumn.IsCreated, "field IsCreated")
	assert.Equal(t, normalColumn.IsUpdated, optionColumn.IsUpdated, "field IsUpdated")
	assert.Equal(t, normalColumn.IsDeleted, optionColumn.IsDeleted, "field IsDeleted")
	assert.Equal(t, normalColumn.IsCascade, optionColumn.IsCascade, "field IsCascade")
	assert.Equal(t, normalColumn.IsVersion, optionColumn.IsVersion, "field IsVersion")
	assert.Equal(t, normalColumn.DefaultIsEmpty, optionColumn.DefaultIsEmpty, "field DefaultIsEmpty")
	assert.Equal(t, normalColumn.EnumOptions, optionColumn.EnumOptions, "field EnumOptions")
	assert.Equal(t, normalColumn.SetOptions, optionColumn.SetOptions, "field SetOptions")
	assert.Equal(t, normalColumn.DisableTimeZone, optionColumn.DisableTimeZone, "field DisableTimeZone")
	assert.Equal(t, normalColumn.TimeZone, optionColumn.TimeZone, "field TimeZone")
	assert.Equal(t, normalColumn.Comment, optionColumn.Comment, "field Comment")
	assert.Equal(t, normalColumn.Collation, optionColumn.Collation, "field Collation")
}
