//go:build integration
// +build integration

package repositoryimp_test

import (
	"context"
	"errors"
	"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/internal/mongo/repositoryimp/builderimp"
	"github.com/tech/sendico/pkg/db/repository/builder"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/testcontainers/testcontainers-go"
	"github.com/testcontainers/testcontainers-go/modules/mongodb"
	"github.com/testcontainers/testcontainers-go/wait"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.mongodb.org/mongo-driver/v2/mongo"
	"go.mongodb.org/mongo-driver/v2/mongo/options"
)

func TestMongoRepository_Insert(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")
	repository := repositoryimp.NewMongoRepository(db, "testcollection")

	t.Run("Insert_WithoutID", func(t *testing.T) {
		testObj := &TestObject{Name: "testInsert"}
		// ID should be nil/zero initially
		assert.True(t, testObj.GetID().IsZero())

		err := repository.Insert(ctx, testObj, nil)
		require.NoError(t, err)

		// ID should be assigned after insert
		assert.False(t, testObj.GetID().IsZero())
		assert.NotEmpty(t, testObj.CreatedAt)
		assert.NotEmpty(t, testObj.UpdatedAt)
	})

	t.Run("Insert_WithExistingID", func(t *testing.T) {
		existingID := bson.NewObjectID()
		testObj := &TestObject{Name: "testInsertWithID"}
		testObj.SetID(existingID)

		err := repository.Insert(ctx, testObj, nil)
		require.NoError(t, err)

		// ID should remain the same
		assert.Equal(t, existingID, *testObj.GetID())
	})

	t.Run("Insert_DuplicateKey", func(t *testing.T) {
		// Insert first object
		testObj1 := &TestObject{Name: "duplicate"}
		err := repository.Insert(ctx, testObj1, nil)
		require.NoError(t, err)

		// Try to insert object with same ID
		testObj2 := &TestObject{Name: "duplicate2"}
		testObj2.SetID(*testObj1.GetID())

		err = repository.Insert(ctx, testObj2, nil)
		assert.Error(t, err)
		assert.True(t, errors.Is(err, merrors.ErrDataConflict))
	})

	t.Run("Insert_DuplicateKeyWithGetFilter", func(t *testing.T) {
		// Insert first object
		testObj1 := &TestObject{Name: "duplicateWithFilter"}
		err := repository.Insert(ctx, testObj1, nil)
		require.NoError(t, err)

		// Try to insert object with same ID, but with getFilter
		testObj2 := &TestObject{Name: "duplicateWithFilter2"}
		testObj2.SetID(*testObj1.GetID())

		getFilter := builderimp.NewQueryImp().Comparison(builderimp.NewFieldImp("_id"), builder.Eq, *testObj1.GetID())

		err = repository.Insert(ctx, testObj2, getFilter)
		assert.Error(t, err)
		assert.True(t, errors.Is(err, merrors.ErrDataConflict))

		// But testObj2 should be populated with the existing object data
		assert.Equal(t, testObj1.Name, testObj2.Name)
	})
}

func TestMongoRepository_Update(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")
	repository := repositoryimp.NewMongoRepository(db, "testcollection")

	t.Run("Update_ExistingObject", func(t *testing.T) {
		// Insert object first
		testObj := &TestObject{Name: "originalName"}
		err := repository.Insert(ctx, testObj, nil)
		require.NoError(t, err)

		originalUpdatedAt := testObj.UpdatedAt

		// Update the object
		testObj.Name = "updatedName"
		time.Sleep(10 * time.Millisecond) // Ensure time difference

		err = repository.Update(ctx, testObj)
		require.NoError(t, err)

		// Verify the object was updated
		result := &TestObject{}
		err = repository.Get(ctx, *testObj.GetID(), result)
		require.NoError(t, err)
		assert.Equal(t, "updatedName", result.Name)
		assert.True(t, result.UpdatedAt.After(originalUpdatedAt))
	})

	t.Run("Update_NonExistentObject", func(t *testing.T) {
		nonExistentID := bson.NewObjectID()
		testObj := &TestObject{Name: "nonExistent"}
		testObj.SetID(nonExistentID)

		err := repository.Update(ctx, testObj)
		assert.Error(t, err)
		assert.True(t, errors.Is(err, mongo.ErrNoDocuments))
	})
}

func TestMongoRepository_Delete(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")
	repository := repositoryimp.NewMongoRepository(db, "testcollection")

	t.Run("Delete_ExistingObject", func(t *testing.T) {
		// Insert object first
		testObj := &TestObject{Name: "toDelete"}
		err := repository.Insert(ctx, testObj, nil)
		require.NoError(t, err)

		// Delete the object
		err = repository.Delete(ctx, *testObj.GetID())
		require.NoError(t, err)

		// Verify the object was deleted
		result := &TestObject{}
		err = repository.Get(ctx, *testObj.GetID(), result)
		assert.Error(t, err)
		assert.True(t, errors.Is(err, merrors.ErrNoData))
	})

	t.Run("Delete_NonExistentObject", func(t *testing.T) {
		nonExistentID := bson.NewObjectID()

		err := repository.Delete(ctx, nonExistentID)
		// Delete should not return error even if object doesn't exist
		assert.NoError(t, err)
	})
}

func TestMongoRepository_FindOneByFilter(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")
	repository := repositoryimp.NewMongoRepository(db, "testcollection")

	t.Run("FindOneByFilter_MatchingFilter", func(t *testing.T) {
		// Insert test objects
		testObjs := []*TestObject{
			{Name: "findMe"},
			{Name: "dontFindMe"},
			{Name: "findMeToo"},
		}

		for _, obj := range testObjs {
			err := repository.Insert(ctx, obj, nil)
			require.NoError(t, err)
		}

		// Find by filter
		query := builderimp.NewQueryImp().Comparison(builderimp.NewFieldImp("name"), builder.Eq, "findMe")
		result := &TestObject{}

		err := repository.FindOneByFilter(ctx, query, result)
		require.NoError(t, err)
		assert.Equal(t, "findMe", result.Name)
	})

	t.Run("FindOneByFilter_NoMatch", func(t *testing.T) {
		query := builderimp.NewQueryImp().Comparison(builderimp.NewFieldImp("name"), builder.Eq, "nonExistentName")
		result := &TestObject{}

		err := repository.FindOneByFilter(ctx, query, result)
		assert.Error(t, err)
		assert.True(t, errors.Is(err, merrors.ErrNoData))
	})
}

func TestMongoRepository_FindManyByFilter(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")
	repository := repositoryimp.NewMongoRepository(db, "testcollection")

	t.Run("FindManyByFilter_MultipleResults", func(t *testing.T) {
		// Insert test objects
		testObjs := []*TestObject{
			{Name: "findMany1"},
			{Name: "findMany2"},
			{Name: "dontFind"},
		}

		for _, obj := range testObjs {
			err := repository.Insert(ctx, obj, nil)
			require.NoError(t, err)
		}

		// Find objects with names starting with "findMany"
		query := builderimp.NewQueryImp().RegEx(builderimp.NewFieldImp("name"), "^findMany", "")

		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 := repository.FindManyByFilter(ctx, query, decoder)
		require.NoError(t, err)
		assert.Len(t, results, 2)

		names := make([]string, len(results))
		for i, obj := range results {
			names[i] = obj.Name
		}
		assert.Contains(t, names, "findMany1")
		assert.Contains(t, names, "findMany2")
	})

	t.Run("FindManyByFilter_NoResults", func(t *testing.T) {
		query := builderimp.NewQueryImp().Comparison(builderimp.NewFieldImp("name"), builder.Eq, "nonExistentPattern")

		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 := repository.FindManyByFilter(ctx, query, decoder)
		require.NoError(t, err)
		assert.Empty(t, results)
	})
}

func TestMongoRepository_DeleteMany(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")
	repository := repositoryimp.NewMongoRepository(db, "testcollection")

	t.Run("DeleteMany_MultipleDocuments", func(t *testing.T) {
		// Insert test objects
		testObjs := []*TestObject{
			{Name: "deleteMany1"},
			{Name: "deleteMany2"},
			{Name: "keepMe"},
		}

		for _, obj := range testObjs {
			err := repository.Insert(ctx, obj, nil)
			require.NoError(t, err)
		}

		// Delete objects with names starting with "deleteMany"
		query := builderimp.NewQueryImp().RegEx(builderimp.NewFieldImp("name"), "^deleteMany", "")

		err := repository.DeleteMany(ctx, query)
		require.NoError(t, err)

		// Verify deletions
		queryAll := builderimp.NewQueryImp()
		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 = repository.FindManyByFilter(ctx, queryAll, decoder)
		require.NoError(t, err)
		assert.Len(t, results, 1)
		assert.Equal(t, "keepMe", results[0].Name)
	})
}

func TestMongoRepository_Name(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")
	repository := repositoryimp.NewMongoRepository(db, "mycollection")

	t.Run("Name_ReturnsCollectionName", func(t *testing.T) {
		name := repository.Name()
		assert.Equal(t, "mycollection", name)
	})
}

func TestMongoRepository_ListPermissionBound(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")
	repository := repositoryimp.NewMongoRepository(db, "testcollection")

	t.Run("ListPermissionBound_WithData", func(t *testing.T) {
		// Insert test objects with permission bound data
		orgID := bson.NewObjectID()

		// Insert documents directly with permission bound fields
		_, err := db.Collection("testcollection").InsertMany(ctx, []interface{}{
			bson.M{
				"_id":             bson.NewObjectID(),
				"organizationRef": orgID,
				"permissionRef":   bson.NewObjectID(),
			},
			bson.M{
				"_id":             bson.NewObjectID(),
				"organizationRef": orgID,
				"permissionRef":   bson.NewObjectID(),
			},
		})
		require.NoError(t, err)

		// Query for permission bound objects
		query := builderimp.NewQueryImp().Comparison(builderimp.NewFieldImp("organizationRef"), builder.Eq, orgID)

		results, err := repository.ListPermissionBound(ctx, query)
		require.NoError(t, err)
		assert.Len(t, results, 2)

		for _, result := range results {
			assert.Equal(t, orgID, result.GetOrganizationRef())
			assert.NotNil(t, result.GetPermissionRef())
		}
	})

	t.Run("ListPermissionBound_EmptyResult", func(t *testing.T) {
		nonExistentOrgID := bson.NewObjectID()
		query := builderimp.NewQueryImp().Comparison(builderimp.NewFieldImp("organizationRef"), builder.Eq, nonExistentOrgID)

		results, err := repository.ListPermissionBound(ctx, query)
		require.NoError(t, err)
		assert.Empty(t, results)
	})
}

func TestMongoRepository_UpdateTimestamp(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")
	repository := repositoryimp.NewMongoRepository(db, "testcollection")

	t.Run("Update_Should_Update_Timestamp", func(t *testing.T) {
		// Create test object
		obj := &TestObject{
			Name: "Test Object",
		}

		// Set ID and initial timestamps
		obj.SetID(bson.NewObjectID())
		originalCreatedAt := obj.CreatedAt
		originalUpdatedAt := obj.UpdatedAt

		// Insert the object
		err := repository.Insert(ctx, obj, nil)
		require.NoError(t, err)

		// Wait a moment to ensure timestamp difference
		time.Sleep(10 * time.Millisecond)

		// Update the object
		obj.Name = "Updated Object"
		err = repository.Update(ctx, obj)
		require.NoError(t, err)

		// Verify timestamps
		assert.Equal(t, originalCreatedAt, obj.CreatedAt, "CreatedAt should not change")
		assert.True(t, obj.UpdatedAt.After(originalUpdatedAt), "UpdatedAt should be updated")

		// Verify the object was actually updated in the database
		var retrieved TestObject
		err = repository.Get(ctx, *obj.GetID(), &retrieved)
		require.NoError(t, err)

		assert.Equal(t, "Updated Object", retrieved.Name, "Name should be updated")
		assert.WithinDuration(t, originalCreatedAt, retrieved.CreatedAt, time.Second, "CreatedAt should not change in DB")
		assert.True(t, retrieved.UpdatedAt.After(originalUpdatedAt), "UpdatedAt should be updated in DB")
		assert.WithinDuration(t, obj.UpdatedAt, retrieved.UpdatedAt, time.Second, "UpdatedAt should match between object and DB")
	})
}
