//go:build integration
// +build integration

package repositoryimp_test

import (
	"context"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/tech/sendico/pkg/db/internal/mongo/repositoryimp"
	"github.com/tech/sendico/pkg/db/repository"
	"github.com/tech/sendico/pkg/db/repository/builder"
	"github.com/testcontainers/testcontainers-go"
	"github.com/testcontainers/testcontainers-go/modules/mongodb"
	"github.com/testcontainers/testcontainers-go/wait"
	"go.mongodb.org/mongo-driver/v2/mongo"
	"go.mongodb.org/mongo-driver/v2/mongo/options"
)

func TestMongoRepository_PatchOperations(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
	defer cancel()

	mongoContainer, err := mongodb.Run(ctx,
		"mongo:latest",
		mongodb.WithUsername("root"),
		mongodb.WithPassword("password"),
		testcontainers.WithWaitStrategy(wait.ForLog("Waiting for connections")),
	)
	require.NoError(t, err, "failed to start MongoDB container")
	defer terminate(ctx, t, mongoContainer)

	mongoURI, err := mongoContainer.ConnectionString(ctx)
	require.NoError(t, err, "failed to get MongoDB connection string")

	clientOptions := options.Client().ApplyURI(mongoURI)
	client, err := mongo.Connect(ctx, clientOptions)
	require.NoError(t, err, "failed to connect to MongoDB")
	defer disconnect(ctx, t, client)

	db := client.Database("testdb")
	repo := repositoryimp.NewMongoRepository(db, "testcollection")

	t.Run("Patch_SingleDocument", func(t *testing.T) {
		obj := &TestObject{Name: "old"}
		err := repo.Insert(ctx, obj, nil)
		require.NoError(t, err)
		original := obj.UpdatedAt

		patch := repository.Patch().Set(repository.Field("name"), "new")
		err = repo.Patch(ctx, *obj.GetID(), patch)
		require.NoError(t, err)

		var result TestObject
		err = repo.Get(ctx, *obj.GetID(), &result)
		require.NoError(t, err)
		assert.Equal(t, "new", result.Name)
		assert.True(t, result.UpdatedAt.After(original))
	})

	t.Run("PatchMany_MultipleDocuments", func(t *testing.T) {
		objs := []*TestObject{{Name: "match"}, {Name: "match"}, {Name: "other"}}
		for _, o := range objs {
			err := repo.Insert(ctx, o, nil)
			require.NoError(t, err)
		}

		query := repository.Query().Comparison(repository.Field("name"), builder.Eq, "match")
		patch := repository.Patch().Set(repository.Field("name"), "patched")
		modified, err := repo.PatchMany(ctx, query, patch)
		require.NoError(t, err)
		assert.Equal(t, 2, modified)

		verify := repository.Query().Comparison(repository.Field("name"), builder.Eq, "patched")
		var results []TestObject
		decoder := func(cursor *mongo.Cursor) error {
			var obj TestObject
			if err := cursor.Decode(&obj); err != nil {
				return err
			}
			results = append(results, obj)
			return nil
		}
		err = repo.FindManyByFilter(ctx, verify, decoder)
		require.NoError(t, err)
		assert.Len(t, results, 2)
	})

	t.Run("Patch_PushArray", func(t *testing.T) {
		obj := &TestObject{Name: "test", Tags: []string{"tag1"}}
		err := repo.Insert(ctx, obj, nil)
		require.NoError(t, err)

		patch := repository.Patch().Push(repository.Field("tags"), "tag2")
		err = repo.Patch(ctx, *obj.GetID(), patch)
		require.NoError(t, err)

		var result TestObject
		err = repo.Get(ctx, *obj.GetID(), &result)
		require.NoError(t, err)
		assert.Equal(t, []string{"tag1", "tag2"}, result.Tags)
	})

	t.Run("Patch_PullArray", func(t *testing.T) {
		obj := &TestObject{Name: "test", Tags: []string{"tag1", "tag2", "tag3"}}
		err := repo.Insert(ctx, obj, nil)
		require.NoError(t, err)

		patch := repository.Patch().Pull(repository.Field("tags"), "tag2")
		err = repo.Patch(ctx, *obj.GetID(), patch)
		require.NoError(t, err)

		var result TestObject
		err = repo.Get(ctx, *obj.GetID(), &result)
		require.NoError(t, err)
		assert.Equal(t, []string{"tag1", "tag3"}, result.Tags)
	})

	t.Run("Patch_AddToSetArray", func(t *testing.T) {
		obj := &TestObject{Name: "test", Tags: []string{"tag1"}}
		err := repo.Insert(ctx, obj, nil)
		require.NoError(t, err)

		// Add new tag
		patch := repository.Patch().AddToSet(repository.Field("tags"), "tag2")
		err = repo.Patch(ctx, *obj.GetID(), patch)
		require.NoError(t, err)

		var result TestObject
		err = repo.Get(ctx, *obj.GetID(), &result)
		require.NoError(t, err)
		assert.Equal(t, []string{"tag1", "tag2"}, result.Tags)

		// Try to add duplicate tag - should not add
		patch = repository.Patch().AddToSet(repository.Field("tags"), "tag1")
		err = repo.Patch(ctx, *obj.GetID(), patch)
		require.NoError(t, err)

		err = repo.Get(ctx, *obj.GetID(), &result)
		require.NoError(t, err)
		assert.Equal(t, []string{"tag1", "tag2"}, result.Tags)
	})

	t.Run("Patch_PushToEmptyArray", func(t *testing.T) {
		obj := &TestObject{Name: "test", Tags: []string{}}
		err := repo.Insert(ctx, obj, nil)
		require.NoError(t, err)

		patch := repository.Patch().Push(repository.Field("tags"), "tag1")
		err = repo.Patch(ctx, *obj.GetID(), patch)
		require.NoError(t, err)

		var result TestObject
		err = repo.Get(ctx, *obj.GetID(), &result)
		require.NoError(t, err)
		assert.Equal(t, []string{"tag1"}, result.Tags)
	})

	t.Run("Patch_PullFromEmptyArray", func(t *testing.T) {
		obj := &TestObject{Name: "test", Tags: []string{}}
		err := repo.Insert(ctx, obj, nil)
		require.NoError(t, err)

		patch := repository.Patch().Pull(repository.Field("tags"), "nonexistent")
		err = repo.Patch(ctx, *obj.GetID(), patch)
		require.NoError(t, err)

		var result TestObject
		err = repo.Get(ctx, *obj.GetID(), &result)
		require.NoError(t, err)
		assert.Equal(t, []string{}, result.Tags)
	})

	t.Run("Patch_PullNonExistentElement", func(t *testing.T) {
		obj := &TestObject{Name: "test", Tags: []string{"tag1", "tag2"}}
		err := repo.Insert(ctx, obj, nil)
		require.NoError(t, err)

		patch := repository.Patch().Pull(repository.Field("tags"), "nonexistent")
		err = repo.Patch(ctx, *obj.GetID(), patch)
		require.NoError(t, err)

		var result TestObject
		err = repo.Get(ctx, *obj.GetID(), &result)
		require.NoError(t, err)
		assert.Equal(t, []string{"tag1", "tag2"}, result.Tags)
	})

	t.Run("Patch_ChainedArrayOperations", func(t *testing.T) {
		obj := &TestObject{Name: "test", Tags: []string{"tag1"}}
		err := repo.Insert(ctx, obj, nil)
		require.NoError(t, err)

		// Note: MongoDB doesn't allow multiple operations on the same array field in a single update
		// This test demonstrates that chained array operations on the same field will fail
		patch := repository.Patch().
			Push(repository.Field("tags"), "tag2").
			AddToSet(repository.Field("tags"), "tag3").
			Pull(repository.Field("tags"), "tag1")
		err = repo.Patch(ctx, *obj.GetID(), patch)
		require.Error(t, err) // This should fail due to MongoDB's limitation
		assert.Contains(t, err.Error(), "conflict")
	})

	t.Run("PatchMany_ArrayOperations", func(t *testing.T) {
		objs := []*TestObject{
			{Name: "obj1", Tags: []string{"tag1"}},
			{Name: "obj2", Tags: []string{"tag2"}},
			{Name: "obj3", Tags: []string{"tag3"}},
		}
		for _, o := range objs {
			err := repo.Insert(ctx, o, nil)
			require.NoError(t, err)
		}

		query := repository.Query().Comparison(repository.Field("name"), builder.In, []string{"obj1", "obj2"})
		patch := repository.Patch().Push(repository.Field("tags"), "common")
		modified, err := repo.PatchMany(ctx, query, patch)
		require.NoError(t, err)
		assert.Equal(t, 2, modified)

		// Verify the changes
		for _, name := range []string{"obj1", "obj2"} {
			var result TestObject
			err = repo.FindOneByFilter(ctx, repository.Query().Comparison(repository.Field("name"), builder.Eq, name), &result)
			require.NoError(t, err)
			assert.Contains(t, result.Tags, "common")
		}
	})
}
