package mon

import (
	"context"
	"testing"

	"github.com/stretchr/testify/assert"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/integration/mtest"
)

func TestModel_StartSession(t *testing.T) {
	mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
	defer mt.Close()

	mt.Run("test", func(mt *mtest.T) {
		m := createModel(mt)
		sess, err := m.StartSession()
		assert.Nil(t, err)
		defer sess.EndSession(context.Background())

		_, err = sess.WithTransaction(context.Background(), func(sessCtx mongo.SessionContext) (any, error) {
			_ = sessCtx.StartTransaction()
			sessCtx.Client().Database("1")
			sessCtx.EndSession(context.Background())
			return nil, nil
		})
		assert.Nil(t, err)
		assert.NoError(t, sess.CommitTransaction(context.Background()))
		assert.Error(t, sess.AbortTransaction(context.Background()))
	})
}

func TestModel_Aggregate(t *testing.T) {
	mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
	defer mt.Close()

	mt.Run("test", func(mt *mtest.T) {
		m := createModel(mt)
		find := mtest.CreateCursorResponse(
			1,
			"DBName.CollectionName",
			mtest.FirstBatch,
			bson.D{
				{Key: "name", Value: "John"},
			})
		getMore := mtest.CreateCursorResponse(
			1,
			"DBName.CollectionName",
			mtest.NextBatch,
			bson.D{
				{Key: "name", Value: "Mary"},
			})
		killCursors := mtest.CreateCursorResponse(
			0,
			"DBName.CollectionName",
			mtest.NextBatch)
		mt.AddMockResponses(find, getMore, killCursors)
		var result []any
		err := m.Aggregate(context.Background(), &result, mongo.Pipeline{})
		assert.Nil(t, err)
		assert.Equal(t, 2, len(result))
		assert.Equal(t, "John", result[0].(bson.D).Map()["name"])
		assert.Equal(t, "Mary", result[1].(bson.D).Map()["name"])

		triggerBreaker(m)
		assert.Equal(t, errDummy, m.Aggregate(context.Background(), &result, mongo.Pipeline{}))
	})
}

func TestModel_DeleteMany(t *testing.T) {
	mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
	defer mt.Close()

	mt.Run("test", func(mt *mtest.T) {
		m := createModel(mt)
		mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{{Key: "n", Value: 1}}...))
		val, err := m.DeleteMany(context.Background(), bson.D{})
		assert.Nil(t, err)
		assert.Equal(t, int64(1), val)

		triggerBreaker(m)
		_, err = m.DeleteMany(context.Background(), bson.D{})
		assert.Equal(t, errDummy, err)
	})
}

func TestModel_DeleteOne(t *testing.T) {
	mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
	defer mt.Close()

	mt.Run("test", func(mt *mtest.T) {
		m := createModel(mt)
		mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{{Key: "n", Value: 1}}...))
		val, err := m.DeleteOne(context.Background(), bson.D{})
		assert.Nil(t, err)
		assert.Equal(t, int64(1), val)

		triggerBreaker(m)
		_, err = m.DeleteOne(context.Background(), bson.D{})
		assert.Equal(t, errDummy, err)
	})
}

func TestModel_Find(t *testing.T) {
	mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
	defer mt.Close()

	mt.Run("test", func(mt *mtest.T) {
		m := createModel(mt)
		find := mtest.CreateCursorResponse(
			1,
			"DBName.CollectionName",
			mtest.FirstBatch,
			bson.D{
				{Key: "name", Value: "John"},
			})
		getMore := mtest.CreateCursorResponse(
			1,
			"DBName.CollectionName",
			mtest.NextBatch,
			bson.D{
				{Key: "name", Value: "Mary"},
			})
		killCursors := mtest.CreateCursorResponse(
			0,
			"DBName.CollectionName",
			mtest.NextBatch)
		mt.AddMockResponses(find, getMore, killCursors)
		var result []any
		err := m.Find(context.Background(), &result, bson.D{})
		assert.Nil(t, err)
		assert.Equal(t, 2, len(result))
		assert.Equal(t, "John", result[0].(bson.D).Map()["name"])
		assert.Equal(t, "Mary", result[1].(bson.D).Map()["name"])

		triggerBreaker(m)
		assert.Equal(t, errDummy, m.Find(context.Background(), &result, bson.D{}))
	})
}

func TestModel_FindOne(t *testing.T) {
	mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
	defer mt.Close()

	mt.Run("test", func(mt *mtest.T) {
		m := createModel(mt)
		find := mtest.CreateCursorResponse(
			1,
			"DBName.CollectionName",
			mtest.FirstBatch,
			bson.D{
				{Key: "name", Value: "John"},
			})
		killCursors := mtest.CreateCursorResponse(
			0,
			"DBName.CollectionName",
			mtest.NextBatch)
		mt.AddMockResponses(find, killCursors)
		var result bson.D
		err := m.FindOne(context.Background(), &result, bson.D{})
		assert.Nil(t, err)
		assert.Equal(t, "John", result.Map()["name"])

		triggerBreaker(m)
		assert.Equal(t, errDummy, m.FindOne(context.Background(), &result, bson.D{}))
	})
}

func TestModel_FindOneAndDelete(t *testing.T) {
	mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
	defer mt.Close()

	mt.Run("test", func(mt *mtest.T) {
		m := createModel(mt)
		mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
			{Key: "value", Value: bson.D{{Key: "name", Value: "John"}}},
		}...))
		var result bson.D
		err := m.FindOneAndDelete(context.Background(), &result, bson.D{})
		assert.Nil(t, err)
		assert.Equal(t, "John", result.Map()["name"])

		triggerBreaker(m)
		assert.Equal(t, errDummy, m.FindOneAndDelete(context.Background(), &result, bson.D{}))
	})
}

func TestModel_FindOneAndReplace(t *testing.T) {
	mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
	defer mt.Close()

	mt.Run("test", func(mt *mtest.T) {
		m := createModel(mt)
		mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
			{Key: "value", Value: bson.D{{Key: "name", Value: "John"}}},
		}...))
		var result bson.D
		err := m.FindOneAndReplace(context.Background(), &result, bson.D{}, bson.D{
			{Key: "name", Value: "Mary"},
		})
		assert.Nil(t, err)
		assert.Equal(t, "John", result.Map()["name"])

		triggerBreaker(m)
		assert.Equal(t, errDummy, m.FindOneAndReplace(context.Background(), &result, bson.D{}, bson.D{
			{Key: "name", Value: "Mary"},
		}))
	})
}

func TestModel_FindOneAndUpdate(t *testing.T) {
	mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
	defer mt.Close()

	mt.Run("test", func(mt *mtest.T) {
		m := createModel(mt)
		mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
			{Key: "value", Value: bson.D{{Key: "name", Value: "John"}}},
		}...))
		var result bson.D
		err := m.FindOneAndUpdate(context.Background(), &result, bson.D{}, bson.D{
			{Key: "$set", Value: bson.D{{Key: "name", Value: "Mary"}}},
		})
		assert.Nil(t, err)
		assert.Equal(t, "John", result.Map()["name"])

		triggerBreaker(m)
		assert.Equal(t, errDummy, m.FindOneAndUpdate(context.Background(), &result, bson.D{}, bson.D{
			{Key: "$set", Value: bson.D{{Key: "name", Value: "Mary"}}},
		}))
	})
}

func createModel(mt *mtest.T) *Model {
	Inject(mt.Name(), mt.Client)
	return MustNewModel(mt.Name(), mt.DB.Name(), mt.Coll.Name())
}

func triggerBreaker(m *Model) {
	m.Collection.(*decoratedCollection).brk = new(dropBreaker)
}
