package monc

import (
	"context"
	"errors"
	"sync/atomic"
	"testing"

	"github.com/alicebob/miniredis/v2"
	"github.com/stretchr/testify/assert"
	"github.com/zeromicro/go-zero/core/stores/cache"
	"github.com/zeromicro/go-zero/core/stores/mon"
	"github.com/zeromicro/go-zero/core/stores/redis"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/mongo/integration/mtest"
)

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

	mt.Run("test", func(mt *mtest.T) {
		_, err := newModel("foo", mt.DB.Name(), mt.Coll.Name(), nil)
		assert.NotNil(mt, err)
	})
}

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

	mt.Run("test", func(mt *mtest.T) {
		m := createModel(t, mt)
		assert.Nil(t, m.cache.Set("foo", "bar"))
		assert.Nil(t, m.cache.Set("bar", "baz"))
		assert.Nil(t, m.DelCache(context.Background(), "foo", "bar"))
		var v string
		assert.True(t, m.cache.IsNotFound(m.cache.Get("foo", &v)))
		assert.True(t, m.cache.IsNotFound(m.cache.Get("bar", &v)))
	})
}

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) {
		mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{{Key: "n", Value: 1}}...))
		m := createModel(t, mt)
		assert.Nil(t, m.cache.Set("foo", "bar"))
		val, err := m.DeleteOne(context.Background(), "foo", bson.D{{Key: "foo", Value: "bar"}})
		assert.Nil(t, err)
		assert.Equal(t, int64(1), val)
		var v string
		assert.True(t, m.cache.IsNotFound(m.cache.Get("foo", &v)))
		_, err = m.DeleteOne(context.Background(), "foo", bson.D{{Key: "foo", Value: "bar"}})
		assert.NotNil(t, err)

		m.cache = mockedCache{m.cache}
		mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{{Key: "n", Value: 1}}...))
		_, err = m.DeleteOne(context.Background(), "foo", bson.D{{Key: "foo", Value: "bar"}})
		assert.Equal(t, errMocked, err)
	})
}

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

	mt.Run("test", func(mt *mtest.T) {
		mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{{Key: "n", Value: 1}}...))
		m := createModel(t, mt)
		assert.Nil(t, m.cache.Set("foo", "bar"))
		val, err := m.DeleteOneNoCache(context.Background(), bson.D{{Key: "foo", Value: "bar"}})
		assert.Nil(t, err)
		assert.Equal(t, int64(1), val)
		var v string
		assert.Nil(t, m.cache.Get("foo", &v))
	})
}

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) {
		resp := mtest.CreateCursorResponse(
			1,
			"DBName.CollectionName",
			mtest.FirstBatch,
			bson.D{
				{Key: "foo", Value: "bar"},
			})
		mt.AddMockResponses(resp)
		m := createModel(t, mt)
		var v struct {
			Foo string `bson:"foo"`
		}
		assert.Nil(t, m.FindOne(context.Background(), "foo", &v, bson.D{}))
		assert.Equal(t, "bar", v.Foo)
		assert.Nil(t, m.cache.Set("foo", "bar"))
	})
}

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

	mt.Run("test", func(mt *mtest.T) {
		resp := mtest.CreateCursorResponse(
			1,
			"DBName.CollectionName",
			mtest.FirstBatch,
			bson.D{
				{Key: "foo", Value: "bar"},
			})
		mt.AddMockResponses(resp)
		m := createModel(t, mt)
		v := struct {
			Foo string `bson:"foo"`
		}{}
		assert.Nil(t, m.FindOneNoCache(context.Background(), &v, bson.D{}))
		assert.Equal(t, "bar", v.Foo)
	})
}

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) {
		mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
			{Key: "value", Value: bson.D{{Key: "foo", Value: "bar"}}},
		}...))
		m := createModel(t, mt)
		assert.Nil(t, m.cache.Set("foo", "bar"))
		v := struct {
			Foo string `bson:"foo"`
		}{}
		assert.Nil(t, m.FindOneAndDelete(context.Background(), "foo", &v, bson.D{}))
		assert.Equal(t, "bar", v.Foo)
		assert.True(t, m.cache.IsNotFound(m.cache.Get("foo", &v)))
		assert.NotNil(t, m.FindOneAndDelete(context.Background(), "foo", &v, bson.D{}))

		m.cache = mockedCache{m.cache}
		mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
			{Key: "value", Value: bson.D{{Key: "foo", Value: "bar"}}},
		}...))
		assert.Equal(t, errMocked, m.FindOneAndDelete(context.Background(), "foo", &v, bson.D{}))
	})
}

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

	mt.Run("test", func(mt *mtest.T) {
		mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
			{Key: "value", Value: bson.D{{Key: "foo", Value: "bar"}}},
		}...))
		m := createModel(t, mt)
		v := struct {
			Foo string `bson:"foo"`
		}{}
		assert.Nil(t, m.FindOneAndDeleteNoCache(context.Background(), &v, bson.D{}))
		assert.Equal(t, "bar", v.Foo)
	})
}

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) {
		mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
			{Key: "value", Value: bson.D{{Key: "foo", Value: "bar"}}},
		}...))
		m := createModel(t, mt)
		assert.Nil(t, m.cache.Set("foo", "bar"))
		v := struct {
			Foo string `bson:"foo"`
		}{}
		assert.Nil(t, m.FindOneAndReplace(context.Background(), "foo", &v, bson.D{}, bson.D{
			{Key: "name", Value: "Mary"},
		}))
		assert.Equal(t, "bar", v.Foo)
		assert.True(t, m.cache.IsNotFound(m.cache.Get("foo", &v)))
		assert.NotNil(t, m.FindOneAndReplace(context.Background(), "foo", &v, bson.D{}, bson.D{
			{Key: "name", Value: "Mary"},
		}))

		m.cache = mockedCache{m.cache}
		mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
			{Key: "value", Value: bson.D{{Key: "foo", Value: "bar"}}},
		}...))
		assert.Equal(t, errMocked, m.FindOneAndReplace(context.Background(), "foo", &v, bson.D{}, bson.D{
			{Key: "name", Value: "Mary"},
		}))
	})
}

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

	mt.Run("test", func(mt *mtest.T) {
		mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
			{Key: "value", Value: bson.D{{Key: "foo", Value: "bar"}}},
		}...))
		m := createModel(t, mt)
		v := struct {
			Foo string `bson:"foo"`
		}{}
		assert.Nil(t, m.FindOneAndReplaceNoCache(context.Background(), &v, bson.D{}, bson.D{
			{Key: "name", Value: "Mary"},
		}))
		assert.Equal(t, "bar", v.Foo)
	})
}

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) {
		mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
			{Key: "value", Value: bson.D{{Key: "foo", Value: "bar"}}},
		}...))
		m := createModel(t, mt)
		assert.Nil(t, m.cache.Set("foo", "bar"))
		v := struct {
			Foo string `bson:"foo"`
		}{}
		assert.Nil(t, m.FindOneAndUpdate(context.Background(), "foo", &v, bson.D{}, bson.D{
			{Key: "$set", Value: bson.D{{Key: "name", Value: "Mary"}}},
		}))
		assert.Equal(t, "bar", v.Foo)
		assert.True(t, m.cache.IsNotFound(m.cache.Get("foo", &v)))
		assert.NotNil(t, m.FindOneAndUpdate(context.Background(), "foo", &v, bson.D{}, bson.D{
			{Key: "$set", Value: bson.D{{Key: "name", Value: "Mary"}}},
		}))

		m.cache = mockedCache{m.cache}
		mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
			{Key: "value", Value: bson.D{{Key: "foo", Value: "bar"}}},
		}...))
		assert.Equal(t, errMocked, m.FindOneAndUpdate(context.Background(), "foo", &v, bson.D{}, bson.D{
			{Key: "$set", Value: bson.D{{Key: "name", Value: "Mary"}}},
		}))
	})
}

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

	mt.Run("test", func(mt *mtest.T) {
		mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
			{Key: "value", Value: bson.D{{Key: "foo", Value: "bar"}}},
		}...))
		m := createModel(t, mt)
		v := struct {
			Foo string `bson:"foo"`
		}{}
		assert.Nil(t, m.FindOneAndUpdateNoCache(context.Background(), &v, bson.D{}, bson.D{
			{Key: "$set", Value: bson.D{{Key: "name", Value: "Mary"}}},
		}))
		assert.Equal(t, "bar", v.Foo)
	})
}

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

	mt.Run("test", func(mt *mtest.T) {
		m := createModel(t, mt)
		assert.NotNil(t, m.cache)
		assert.Nil(t, m.cache.Set("foo", "bar"))
		var s string
		assert.Nil(t, m.cache.Get("foo", &s))
		assert.Equal(t, "bar", s)
	})
}

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

	mt.Run("test", func(mt *mtest.T) {
		mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
			{Key: "value", Value: bson.D{{Key: "foo", Value: "bar"}}},
		}...))
		m := createModel(t, mt)
		assert.Nil(t, m.cache.Set("foo", "bar"))
		resp, err := m.InsertOne(context.Background(), "foo", bson.D{
			{Key: "name", Value: "Mary"},
		})
		assert.Nil(t, err)
		assert.NotNil(t, resp)
		var v string
		assert.True(t, m.cache.IsNotFound(m.cache.Get("foo", &v)))
		_, err = m.InsertOne(context.Background(), "foo", bson.D{
			{Key: "name", Value: "Mary"},
		})
		assert.NotNil(t, err)

		m.cache = mockedCache{m.cache}
		mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
			{Key: "value", Value: bson.D{{Key: "foo", Value: "bar"}}},
		}...))
		_, err = m.InsertOne(context.Background(), "foo", bson.D{
			{Key: "name", Value: "Mary"},
		})
		assert.Equal(t, errMocked, err)
	})
}

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

	mt.Run("test", func(mt *mtest.T) {
		mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
			{Key: "value", Value: bson.D{{Key: "foo", Value: "bar"}}},
		}...))
		m := createModel(t, mt)
		resp, err := m.InsertOneNoCache(context.Background(), bson.D{
			{Key: "name", Value: "Mary"},
		})
		assert.Nil(t, err)
		assert.NotNil(t, resp)
	})
}

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

	mt.Run("test", func(mt *mtest.T) {
		mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
			{Key: "value", Value: bson.D{{Key: "foo", Value: "bar"}}},
		}...))
		m := createModel(t, mt)
		assert.Nil(t, m.cache.Set("foo", "bar"))
		resp, err := m.ReplaceOne(context.Background(), "foo", bson.D{}, bson.D{
			{Key: "foo", Value: "baz"},
		})
		assert.Nil(t, err)
		assert.NotNil(t, resp)
		var v string
		assert.True(t, m.cache.IsNotFound(m.cache.Get("foo", &v)))
		_, err = m.ReplaceOne(context.Background(), "foo", bson.D{}, bson.D{
			{Key: "foo", Value: "baz"},
		})
		assert.NotNil(t, err)

		m.cache = mockedCache{m.cache}
		mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
			{Key: "value", Value: bson.D{{Key: "foo", Value: "bar"}}},
		}...))
		_, err = m.ReplaceOne(context.Background(), "foo", bson.D{}, bson.D{
			{Key: "foo", Value: "baz"},
		})
		assert.Equal(t, errMocked, err)
	})
}

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

	mt.Run("test", func(mt *mtest.T) {
		mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
			{Key: "value", Value: bson.D{{Key: "foo", Value: "bar"}}},
		}...))
		m := createModel(t, mt)
		resp, err := m.ReplaceOneNoCache(context.Background(), bson.D{}, bson.D{
			{Key: "foo", Value: "baz"},
		})
		assert.Nil(t, err)
		assert.NotNil(t, resp)
	})
}

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

	mt.Run("test", func(mt *mtest.T) {
		m := createModel(t, mt)
		assert.Nil(t, m.SetCache("foo", "bar"))
		var v string
		assert.Nil(t, m.GetCache("foo", &v))
		assert.Equal(t, "bar", v)
	})
}

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

	mt.Run("test", func(mt *mtest.T) {
		mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
			{Key: "value", Value: bson.D{{Key: "foo", Value: "bar"}}},
		}...))
		m := createModel(t, mt)
		assert.Nil(t, m.cache.Set("foo", "bar"))
		resp, err := m.UpdateByID(context.Background(), "foo", bson.D{}, bson.D{
			{Key: "$set", Value: bson.D{{Key: "foo", Value: "baz"}}},
		})
		assert.Nil(t, err)
		assert.NotNil(t, resp)
		var v string
		assert.True(t, m.cache.IsNotFound(m.cache.Get("foo", &v)))
		_, err = m.UpdateByID(context.Background(), "foo", bson.D{}, bson.D{
			{Key: "$set", Value: bson.D{{Key: "foo", Value: "baz"}}},
		})
		assert.NotNil(t, err)

		m.cache = mockedCache{m.cache}
		mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
			{Key: "value", Value: bson.D{{Key: "foo", Value: "bar"}}},
		}...))
		_, err = m.UpdateByID(context.Background(), "foo", bson.D{}, bson.D{
			{Key: "$set", Value: bson.D{{Key: "foo", Value: "baz"}}},
		})
		assert.Equal(t, errMocked, err)
	})
}

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

	mt.Run("test", func(mt *mtest.T) {
		mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
			{Key: "value", Value: bson.D{{Key: "foo", Value: "bar"}}},
		}...))
		m := createModel(t, mt)
		resp, err := m.UpdateByIDNoCache(context.Background(), bson.D{}, bson.D{
			{Key: "$set", Value: bson.D{{Key: "foo", Value: "baz"}}},
		})
		assert.Nil(t, err)
		assert.NotNil(t, resp)
	})
}

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

	mt.Run("test", func(mt *mtest.T) {
		mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
			{Key: "value", Value: bson.D{{Key: "foo", Value: "bar"}}},
		}...))
		m := createModel(t, mt)
		assert.Nil(t, m.cache.Set("foo", "bar"))
		assert.Nil(t, m.cache.Set("bar", "baz"))
		resp, err := m.UpdateMany(context.Background(), []string{"foo", "bar"}, bson.D{}, bson.D{
			{Key: "$set", Value: bson.D{{Key: "foo", Value: "baz"}}},
		})
		assert.Nil(t, err)
		assert.NotNil(t, resp)
		var v string
		assert.True(t, m.cache.IsNotFound(m.cache.Get("foo", &v)))
		assert.True(t, m.cache.IsNotFound(m.cache.Get("bar", &v)))
		_, err = m.UpdateMany(context.Background(), []string{"foo", "bar"}, bson.D{}, bson.D{
			{Key: "$set", Value: bson.D{{Key: "foo", Value: "baz"}}},
		})
		assert.NotNil(t, err)

		m.cache = mockedCache{m.cache}
		mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
			{Key: "value", Value: bson.D{{Key: "foo", Value: "bar"}}},
		}...))
		_, err = m.UpdateMany(context.Background(), []string{"foo", "bar"}, bson.D{}, bson.D{
			{Key: "$set", Value: bson.D{{Key: "foo", Value: "baz"}}},
		})
		assert.Equal(t, errMocked, err)
	})
}

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

	mt.Run("test", func(mt *mtest.T) {
		mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
			{Key: "value", Value: bson.D{{Key: "foo", Value: "bar"}}},
		}...))
		m := createModel(t, mt)
		resp, err := m.UpdateManyNoCache(context.Background(), bson.D{}, bson.D{
			{Key: "$set", Value: bson.D{{Key: "foo", Value: "baz"}}},
		})
		assert.Nil(t, err)
		assert.NotNil(t, resp)
	})
}

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

	mt.Run("test", func(mt *mtest.T) {
		mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
			{Key: "value", Value: bson.D{{Key: "foo", Value: "bar"}}},
		}...))
		m := createModel(t, mt)
		assert.Nil(t, m.cache.Set("foo", "bar"))
		resp, err := m.UpdateOne(context.Background(), "foo", bson.D{}, bson.D{
			{Key: "$set", Value: bson.D{{Key: "foo", Value: "baz"}}},
		})
		assert.Nil(t, err)
		assert.NotNil(t, resp)
		var v string
		assert.True(t, m.cache.IsNotFound(m.cache.Get("foo", &v)))
		_, err = m.UpdateOne(context.Background(), "foo", bson.D{}, bson.D{
			{Key: "$set", Value: bson.D{{Key: "foo", Value: "baz"}}},
		})
		assert.NotNil(t, err)

		mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
			{Key: "value", Value: bson.D{{Key: "foo", Value: "bar"}}},
		}...))
		m.cache = mockedCache{m.cache}
		_, err = m.UpdateOne(context.Background(), "foo", bson.D{}, bson.D{
			{Key: "$set", Value: bson.D{{Key: "foo", Value: "baz"}}},
		})
		assert.Equal(t, errMocked, err)
	})
}

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

	mt.Run("test", func(mt *mtest.T) {
		mt.AddMockResponses(mtest.CreateSuccessResponse(bson.D{
			{Key: "value", Value: bson.D{{Key: "foo", Value: "bar"}}},
		}...))
		m := createModel(t, mt)
		resp, err := m.UpdateOneNoCache(context.Background(), bson.D{}, bson.D{
			{Key: "$set", Value: bson.D{{Key: "foo", Value: "baz"}}},
		})
		assert.Nil(t, err)
		assert.NotNil(t, resp)
	})
}

func createModel(t *testing.T, mt *mtest.T) *Model {
	s, err := miniredis.Run()
	assert.Nil(t, err)
	mon.Inject(mt.Name(), mt.Client)
	if atomic.AddInt32(&index, 1)%2 == 0 {
		return MustNewNodeModel(mt.Name(), mt.DB.Name(), mt.Coll.Name(), redis.New(s.Addr()))
	} else {
		return MustNewModel(mt.Name(), mt.DB.Name(), mt.Coll.Name(), cache.CacheConf{
			cache.NodeConf{
				RedisConf: redis.RedisConf{
					Host: s.Addr(),
					Type: redis.NodeType,
				},
				Weight: 100,
			},
		})
	}
}

var (
	errMocked = errors.New("mocked error")
	index     int32
)

type mockedCache struct {
	cache.Cache
}

func (m mockedCache) DelCtx(_ context.Context, _ ...string) error {
	return errMocked
}
