//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/db/storable"
	"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"
)

type TestObject struct {
	storable.Base `bson:",inline" json:",inline"`
	Name          string   `bson:"name"`
	Tags          []string `bson:"tags"`
}

func (t *TestObject) Collection() string {
	return "testObject"
}

type AnotherObject struct {
	storable.Base `bson:",inline" json:",inline"`
	Description   string `bson:"description"`
}

func (a *AnotherObject) Collection() string {
	return "anotherObject"
}

func terminate(ctx context.Context, t *testing.T, container *mongodb.MongoDBContainer) {
	err := container.Terminate(ctx)
	require.NoError(t, err, "failed to terminate MongoDB container")
}

func disconnect(ctx context.Context, t *testing.T, client *mongo.Client) {
	err := client.Disconnect(ctx)
	require.NoError(t, err, "failed to disconnect from MongoDB")
}

func TestMongoRepository_Get(t *testing.T) {
	// Use a context with timeout, so if container spinning or DB ops hang,
	// the test won't run indefinitely.
	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("Get_Success", func(t *testing.T) {
		testObj := &TestObject{Name: "testName"}
		err := repository.Insert(ctx, testObj, nil)
		require.NoError(t, err)

		result := &TestObject{}
		err = repository.Get(ctx, testObj.ID, result)
		require.NoError(t, err)
		assert.Equal(t, testObj.Name, result.Name)
		assert.Equal(t, testObj.ID, result.ID)
	})

	t.Run("Get_NotFound", func(t *testing.T) {
		nonExistentID := bson.NewObjectID()
		result := &TestObject{}

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

	t.Run("Get_InvalidID", func(t *testing.T) {
		invalidID := bson.ObjectID{} // zero value
		result := &TestObject{}

		err := repository.Get(ctx, invalidID, result)
		assert.Error(t, err)
		assert.True(t, errors.Is(err, merrors.ErrInvalidArg))
	})

	t.Run("Get_DifferentTypes", func(t *testing.T) {
		anotherObj := &AnotherObject{Description: "testDescription"}
		err := repository.Insert(ctx, anotherObj, nil)
		require.NoError(t, err)

		result := &AnotherObject{}
		err = repository.Get(ctx, anotherObj.ID, result)
		require.NoError(t, err)
		assert.Equal(t, anotherObj.Description, result.Description)
		assert.Equal(t, anotherObj.ID, result.ID)
	})
}

func TestMongoRepository_ListIDs(t *testing.T) {
	// Use a context with timeout, so if container spinning or DB ops hang,
	// the test won't run indefinitely.
	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("ListIDs_Success", func(t *testing.T) {
		// Insert test data
		testObjs := []*TestObject{
			{Name: "testName1"},
			{Name: "testName2"},
			{Name: "testName3"},
		}
		for _, obj := range testObjs {
			err := repository.Insert(ctx, obj, nil)
			require.NoError(t, err)
		}

		// Define a query to match all objects
		query := builderimp.NewQueryImp()

		// Call ListIDs
		ids, err := repository.ListIDs(ctx, query)
		require.NoError(t, err)

		// Assert the IDs are correct
		require.Len(t, ids, len(testObjs))
		for _, obj := range testObjs {
			assert.Contains(t, ids, obj.ID)
		}
	})

	t.Run("ListIDs_EmptyResult", func(t *testing.T) {
		// Define a query that matches no objects
		query := builderimp.NewQueryImp().Comparison(builderimp.NewFieldImp("name"), builder.Eq, "nonExistentName")

		// Call ListIDs
		ids, err := repository.ListIDs(ctx, query)
		require.NoError(t, err)

		// Assert no IDs are returned
		assert.Empty(t, ids)
	})
}
