//go:build integration
// +build integration

package policiesdb_test

import (
	"context"
	"errors"
	"testing"
	"time"

	// Your internal packages
	"github.com/tech/sendico/pkg/db/internal/mongo/policiesdb"
	"github.com/tech/sendico/pkg/db/repository"
	"github.com/tech/sendico/pkg/db/repository/builder"
	"github.com/tech/sendico/pkg/merrors"

	// Model package (contains PolicyDescription + Describable)
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/tech/sendico/pkg/model"

	// Testcontainers
	"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"
	"go.uber.org/zap"
)

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

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

// Helper to drop the Policies collection
func cleanupCollection(t *testing.T, ctx context.Context, db *mongo.Database) {
	// The actual collection name is typically the value returned by
	// (&model.PolicyDescription{}).Collection(), or something similar.
	// Make sure it matches what your code uses (often "policies" or "policyDescription").
	err := db.Collection((&model.PolicyDescription{}).Collection()).Drop(ctx)
	require.NoError(t, err, "failed to drop collection between sub-tests")
}

func TestPoliciesDB(t *testing.T) {
	// Create context with reasonable timeout
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
	defer cancel()

	// Start MongoDB test container
	mongoC, 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(t, ctx, mongoC)

	// Get connection URI
	mongoURI, err := mongoC.ConnectionString(ctx)
	require.NoError(t, err, "failed to get connection string")

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

	// Create test DB
	db := client.Database("testdb")

	// Use a no-op logger (or real logger if you prefer)
	logger := zap.NewNop()

	// Create an instance of PoliciesDB
	pdb, err := policiesdb.Create(logger, db)
	require.NoError(t, err, "unexpected error creating PoliciesDB")

	// ---------------------------------------------------------
	// Each sub-test below starts by dropping the collection.
	// ---------------------------------------------------------

	t.Run("CreateAndGet", func(t *testing.T) {
		cleanupCollection(t, ctx, db) // ensure no leftover data

		desc := "Test policy description"
		policy := &model.PolicyDescription{
			Describable: model.Describable{
				Name:        "TestPolicy",
				Description: &desc,
			},
		}
		require.NoError(t, pdb.Create(ctx, policy))

		result := &model.PolicyDescription{}
		err := pdb.Get(ctx, policy.ID, result)
		require.NoError(t, err)

		assert.Equal(t, policy.ID, result.ID)
		assert.Equal(t, "TestPolicy", result.Name)
		assert.NotNil(t, result.Description)
		assert.Equal(t, "Test policy description", *result.Description)
	})

	t.Run("Get_NotFound", func(t *testing.T) {
		cleanupCollection(t, ctx, db)

		// Attempt to get a non-existent ID
		nonExistentID := bson.NewObjectID()
		result := &model.PolicyDescription{}
		err := pdb.Get(ctx, nonExistentID, result)
		assert.Error(t, err)
		assert.True(t, errors.Is(err, merrors.ErrNoData))
	})

	t.Run("Update", func(t *testing.T) {
		cleanupCollection(t, ctx, db)

		originalDesc := "Original description"
		policy := &model.PolicyDescription{
			Describable: model.Describable{
				Name:        "OriginalName",
				Description: &originalDesc,
			},
		}
		require.NoError(t, pdb.Create(ctx, policy))

		newDesc := "Updated description"
		policy.Name = "UpdatedName"
		policy.Description = &newDesc

		err := pdb.Update(ctx, policy)
		require.NoError(t, err)

		updated := &model.PolicyDescription{}
		err = pdb.Get(ctx, policy.ID, updated)
		require.NoError(t, err)

		assert.Equal(t, "UpdatedName", updated.Name)
		assert.NotNil(t, updated.Description)
		assert.Equal(t, "Updated description", *updated.Description)
	})

	t.Run("Delete", func(t *testing.T) {
		cleanupCollection(t, ctx, db)

		desc := "To be deleted"
		policy := &model.PolicyDescription{
			Describable: model.Describable{
				Name:        "WillDelete",
				Description: &desc,
			},
		}
		require.NoError(t, pdb.Create(ctx, policy))

		err := pdb.Delete(ctx, policy.ID)
		require.NoError(t, err)

		deleted := &model.PolicyDescription{}
		err = pdb.Get(ctx, policy.ID, deleted)
		assert.Error(t, err)
		assert.True(t, errors.Is(err, merrors.ErrNoData))
	})

	t.Run("DeleteMany", func(t *testing.T) {
		cleanupCollection(t, ctx, db)

		desc1 := "Will be deleted 1"
		desc2 := "Will be deleted 2"
		pol1 := &model.PolicyDescription{
			Describable: model.Describable{
				Name:        "BatchDelete1",
				Description: &desc1,
			},
		}
		pol2 := &model.PolicyDescription{
			Describable: model.Describable{
				Name:        "BatchDelete2",
				Description: &desc2,
			},
		}
		require.NoError(t, pdb.Create(ctx, pol1))
		require.NoError(t, pdb.Create(ctx, pol2))

		q := repository.Query().RegEx(repository.Field("description"), "^Will be deleted", "")
		err := pdb.DeleteMany(ctx, q)
		require.NoError(t, err)

		res1 := &model.PolicyDescription{}
		err1 := pdb.Get(ctx, pol1.ID, res1)
		assert.Error(t, err1)
		assert.True(t, errors.Is(err1, merrors.ErrNoData))

		res2 := &model.PolicyDescription{}
		err2 := pdb.Get(ctx, pol2.ID, res2)
		assert.Error(t, err2)
		assert.True(t, errors.Is(err2, merrors.ErrNoData))
	})

	t.Run("FindOne", func(t *testing.T) {
		cleanupCollection(t, ctx, db)

		desc := "Unique find test"
		policy := &model.PolicyDescription{
			Describable: model.Describable{
				Name:        "FindOneTest",
				Description: &desc,
			},
		}
		require.NoError(t, pdb.Create(ctx, policy))

		// Match by name == "FindOneTest"
		q := repository.Query().Comparison(repository.Field("name"), builder.Eq, "FindOneTest")

		found := &model.PolicyDescription{}
		err := pdb.FindOne(ctx, q, found)
		require.NoError(t, err)

		assert.Equal(t, policy.ID, found.ID)
		assert.Equal(t, "FindOneTest", found.Name)
		assert.NotNil(t, found.Description)
		assert.Equal(t, "Unique find test", *found.Description)
	})

	t.Run("All", func(t *testing.T) {
		cleanupCollection(t, ctx, db)

		// Insert some policies (orgA, orgB, nil org)
		orgA := bson.NewObjectID()
		orgB := bson.NewObjectID()

		descA := "Org A policy"
		policyA := &model.PolicyDescription{
			Describable: model.Describable{
				Name:        "PolicyA",
				Description: &descA,
			},
			OrganizationRef: &orgA, // belongs to orgA
		}
		descB := "Org B policy"
		policyB := &model.PolicyDescription{
			Describable: model.Describable{
				Name:        "PolicyB",
				Description: &descB,
			},
			OrganizationRef: &orgB, // belongs to orgB
		}
		descNil := "No org policy"
		policyNil := &model.PolicyDescription{
			Describable: model.Describable{
				Name:        "PolicyNil",
				Description: &descNil,
			},
			// nil => built-in
		}
		require.NoError(t, pdb.Create(ctx, policyA))
		require.NoError(t, pdb.Create(ctx, policyB))
		require.NoError(t, pdb.Create(ctx, policyNil))

		// Suppose the requirement is: "All" returns
		// - policies for the requested org
		// - plus built-in (nil) ones
		resultsA, err := pdb.All(ctx, orgA)
		require.NoError(t, err)
		require.Len(t, resultsA, 2) // orgA + built-in

		var idsA []bson.ObjectID
		for _, r := range resultsA {
			idsA = append(idsA, r.ID)
		}
		assert.Contains(t, idsA, policyA.ID)
		assert.Contains(t, idsA, policyNil.ID)
		assert.NotContains(t, idsA, policyB.ID)

		resultsB, err := pdb.All(ctx, orgB)
		require.NoError(t, err)
		require.Len(t, resultsB, 2) // orgB + built-in

		var idsB []bson.ObjectID
		for _, r := range resultsB {
			idsB = append(idsB, r.ID)
		}
		assert.Contains(t, idsB, policyB.ID)
		assert.Contains(t, idsB, policyNil.ID)
		assert.NotContains(t, idsB, policyA.ID)
	})

	t.Run("Policies", func(t *testing.T) {
		cleanupCollection(t, ctx, db)

		desc1 := "PolicyOne"
		pol1 := &model.PolicyDescription{
			Describable: model.Describable{
				Name:        "PolicyOne",
				Description: &desc1,
			},
		}
		desc2 := "PolicyTwo"
		pol2 := &model.PolicyDescription{
			Describable: model.Describable{
				Name:        "PolicyTwo",
				Description: &desc2,
			},
		}
		desc3 := "PolicyThree"
		pol3 := &model.PolicyDescription{
			Describable: model.Describable{
				Name:        "PolicyThree",
				Description: &desc3,
			},
		}
		require.NoError(t, pdb.Create(ctx, pol1))
		require.NoError(t, pdb.Create(ctx, pol2))
		require.NoError(t, pdb.Create(ctx, pol3))

		// 1) Request pol1, pol2
		results12, err := pdb.Policies(ctx, []bson.ObjectID{pol1.ID, pol2.ID})
		require.NoError(t, err)
		require.Len(t, results12, 2)
		// IDs might be out of order, so we do a set-like check
		var set12 []bson.ObjectID
		for _, r := range results12 {
			set12 = append(set12, r.ID)
		}
		assert.Contains(t, set12, pol1.ID)
		assert.Contains(t, set12, pol2.ID)

		// 2) Request pol1, pol3, plus a random ID
		fakeID := bson.NewObjectID()
		results13Fake, err := pdb.Policies(ctx, []bson.ObjectID{pol1.ID, pol3.ID, fakeID})
		require.NoError(t, err)
		require.Len(t, results13Fake, 2) // pol1 + pol3 only
		var set13Fake []bson.ObjectID
		for _, r := range results13Fake {
			set13Fake = append(set13Fake, r.ID)
		}
		assert.Contains(t, set13Fake, pol1.ID)
		assert.Contains(t, set13Fake, pol3.ID)

		// 3) Request with empty slice => expect no results
		resultsEmpty, err := pdb.Policies(ctx, []bson.ObjectID{})
		require.NoError(t, err)
		assert.Len(t, resultsEmpty, 0)
	})
}
