package store

import (
	"context"
	"errors"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/tech/sendico/ledger/storage"
	"github.com/tech/sendico/ledger/storage/model"
	"github.com/tech/sendico/pkg/db/repository/builder"
	"github.com/tech/sendico/pkg/db/storable"
	"github.com/tech/sendico/pkg/merrors"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.uber.org/zap"
)

func TestBalancesStore_Get(t *testing.T) {
	ctx := context.Background()
	logger := zap.NewNop()

	t.Run("Success", func(t *testing.T) {
		accountRef := bson.NewObjectID()
		stub := &repositoryStub{
			FindOneByFilterFunc: func(ctx context.Context, _ builder.Query, result storable.Storable) error {
				balance := result.(*model.AccountBalance)
				balance.AccountRef = accountRef
				balance.Balance = "1500.50"
				balance.Version = 10
				return nil
			},
		}

		store := &balancesStore{logger: logger, repo: stub}
		result, err := store.Get(ctx, accountRef)

		require.NoError(t, err)
		assert.NotNil(t, result)
		assert.Equal(t, accountRef, result.AccountRef)
		assert.Equal(t, "1500.50", result.Balance)
		assert.Equal(t, int64(10), result.Version)
	})

	t.Run("ZeroAccountID", func(t *testing.T) {
		stub := &repositoryStub{}
		store := &balancesStore{logger: logger, repo: stub}

		result, err := store.Get(ctx, bson.NilObjectID)

		require.Error(t, err)
		assert.Nil(t, result)
		assert.True(t, errors.Is(err, merrors.ErrInvalidArg))
	})

	t.Run("NotFound", func(t *testing.T) {
		accountRef := bson.NewObjectID()
		stub := &repositoryStub{
			FindOneByFilterFunc: func(ctx context.Context, _ builder.Query, result storable.Storable) error {
				return merrors.ErrNoData
			},
		}

		store := &balancesStore{logger: logger, repo: stub}
		result, err := store.Get(ctx, accountRef)

		require.Error(t, err)
		assert.Nil(t, result)
		assert.True(t, errors.Is(err, storage.ErrBalanceNotFound))
	})

	t.Run("FindError", func(t *testing.T) {
		accountRef := bson.NewObjectID()
		expectedErr := errors.New("database error")
		stub := &repositoryStub{
			FindOneByFilterFunc: func(ctx context.Context, _ builder.Query, result storable.Storable) error {
				return expectedErr
			},
		}

		store := &balancesStore{logger: logger, repo: stub}
		result, err := store.Get(ctx, accountRef)

		require.Error(t, err)
		assert.Nil(t, result)
		assert.Equal(t, expectedErr, err)
	})
}

func TestBalancesStore_Upsert(t *testing.T) {
	ctx := context.Background()
	logger := zap.NewNop()

	t.Run("Insert_NewBalance", func(t *testing.T) {
		accountRef := bson.NewObjectID()
		var insertedBalance *model.AccountBalance

		stub := &repositoryStub{
			FindOneByFilterFunc: func(ctx context.Context, _ builder.Query, result storable.Storable) error {
				return merrors.ErrNoData // Balance doesn't exist
			},
			InsertFunc: func(ctx context.Context, object storable.Storable, _ builder.Query) error {
				insertedBalance = object.(*model.AccountBalance)
				return nil
			},
		}

		store := &balancesStore{logger: logger, repo: stub}
		balance := &model.AccountBalance{
			AccountRef: accountRef,
			Balance:    "1000.00",
			Version:    1,
		}

		err := store.Upsert(ctx, balance)

		require.NoError(t, err)
		assert.NotNil(t, insertedBalance)
		assert.Equal(t, "1000.00", insertedBalance.Balance)
	})

	t.Run("Update_ExistingBalance", func(t *testing.T) {
		accountRef := bson.NewObjectID()
		existingID := bson.NewObjectID()
		var updatedBalance *model.AccountBalance

		stub := &repositoryStub{
			FindOneByFilterFunc: func(ctx context.Context, _ builder.Query, result storable.Storable) error {
				existing := result.(*model.AccountBalance)
				existing.SetID(existingID)
				existing.AccountRef = accountRef
				existing.Balance = "500.00"
				existing.Version = 5
				return nil
			},
			UpdateFunc: func(ctx context.Context, object storable.Storable) error {
				updatedBalance = object.(*model.AccountBalance)
				return nil
			},
		}

		store := &balancesStore{logger: logger, repo: stub}
		balance := &model.AccountBalance{
			AccountRef: accountRef,
			Balance:    "1500.00",
			Version:    6,
		}

		err := store.Upsert(ctx, balance)

		require.NoError(t, err)
		assert.NotNil(t, updatedBalance)
		assert.Equal(t, existingID, *updatedBalance.GetID())
		assert.Equal(t, "1500.00", updatedBalance.Balance)
		assert.Equal(t, int64(6), updatedBalance.Version)
	})

	t.Run("NilBalance", func(t *testing.T) {
		stub := &repositoryStub{}
		store := &balancesStore{logger: logger, repo: stub}

		err := store.Upsert(ctx, nil)

		require.Error(t, err)
		assert.True(t, errors.Is(err, merrors.ErrInvalidArg))
	})

	t.Run("ZeroAccountID", func(t *testing.T) {
		stub := &repositoryStub{}
		store := &balancesStore{logger: logger, repo: stub}

		balance := &model.AccountBalance{
			AccountRef: bson.NilObjectID,
			Balance:    "100.00",
		}

		err := store.Upsert(ctx, balance)

		require.Error(t, err)
		assert.True(t, errors.Is(err, merrors.ErrInvalidArg))
	})

	t.Run("FindError", func(t *testing.T) {
		accountRef := bson.NewObjectID()
		expectedErr := errors.New("database error")

		stub := &repositoryStub{
			FindOneByFilterFunc: func(ctx context.Context, _ builder.Query, result storable.Storable) error {
				return expectedErr
			},
		}

		store := &balancesStore{logger: logger, repo: stub}
		balance := &model.AccountBalance{
			AccountRef: accountRef,
			Balance:    "100.00",
		}

		err := store.Upsert(ctx, balance)

		require.Error(t, err)
		assert.Equal(t, expectedErr, err)
	})

	t.Run("InsertError", func(t *testing.T) {
		accountRef := bson.NewObjectID()
		expectedErr := errors.New("insert error")

		stub := &repositoryStub{
			FindOneByFilterFunc: func(ctx context.Context, _ builder.Query, result storable.Storable) error {
				return merrors.ErrNoData // Balance doesn't exist
			},
			InsertFunc: func(ctx context.Context, object storable.Storable, _ builder.Query) error {
				return expectedErr
			},
		}

		store := &balancesStore{logger: logger, repo: stub}
		balance := &model.AccountBalance{
			AccountRef: accountRef,
			Balance:    "100.00",
		}

		err := store.Upsert(ctx, balance)

		require.Error(t, err)
		assert.Equal(t, expectedErr, err)
	})

	t.Run("UpdateError", func(t *testing.T) {
		accountRef := bson.NewObjectID()
		existingID := bson.NewObjectID()
		expectedErr := errors.New("update error")

		stub := &repositoryStub{
			FindOneByFilterFunc: func(ctx context.Context, _ builder.Query, result storable.Storable) error {
				existing := result.(*model.AccountBalance)
				existing.SetID(existingID)
				existing.AccountRef = accountRef
				existing.Balance = "500.00"
				return nil
			},
			UpdateFunc: func(ctx context.Context, object storable.Storable) error {
				return expectedErr
			},
		}

		store := &balancesStore{logger: logger, repo: stub}
		balance := &model.AccountBalance{
			AccountRef: accountRef,
			Balance:    "1500.00",
		}

		err := store.Upsert(ctx, balance)

		require.Error(t, err)
		assert.Equal(t, expectedErr, err)
	})
}

func TestBalancesStore_IncrementBalance(t *testing.T) {
	ctx := context.Background()
	logger := zap.NewNop()

	t.Run("NotImplemented", func(t *testing.T) {
		accountRef := bson.NewObjectID()
		stub := &repositoryStub{}
		store := &balancesStore{logger: logger, repo: stub}

		err := store.IncrementBalance(ctx, accountRef, "100.00")

		require.Error(t, err)
		assert.True(t, errors.Is(err, merrors.ErrNotImplemented))
	})

	t.Run("ZeroAccountID", func(t *testing.T) {
		stub := &repositoryStub{}
		store := &balancesStore{logger: logger, repo: stub}

		err := store.IncrementBalance(ctx, bson.NilObjectID, "100.00")

		require.Error(t, err)
		assert.True(t, errors.Is(err, merrors.ErrInvalidArg))
	})
}
