package store

import (
	"context"
	"errors"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/tech/sendico/ledger/storage/model"
	"github.com/tech/sendico/pkg/db/repository/builder"
	rd "github.com/tech/sendico/pkg/db/repository/decoder"
	"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 TestPostingLinesStore_CreateMany(t *testing.T) {
	ctx := context.Background()
	logger := zap.NewNop()

	t.Run("Success", func(t *testing.T) {
		var insertedLines []storable.Storable
		stub := &repositoryStub{
			InsertManyFunc: func(ctx context.Context, objects []storable.Storable) error {
				insertedLines = objects
				return nil
			},
		}

		store := &postingLinesStore{logger: logger, repo: stub}
		lines := []*model.PostingLine{
			{
				JournalEntryRef: bson.NewObjectID(),
				AccountRef:      bson.NewObjectID(),
				LineType:        model.LineTypeMain,
				Amount:          "100.00",
			},
			{
				JournalEntryRef: bson.NewObjectID(),
				AccountRef:      bson.NewObjectID(),
				LineType:        model.LineTypeMain,
				Amount:          "100.00",
			},
		}

		err := store.CreateMany(ctx, lines)

		require.NoError(t, err)
		assert.Len(t, insertedLines, 2)
	})

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

		err := store.CreateMany(ctx, []*model.PostingLine{})

		require.NoError(t, err) // Should not error on empty array
	})

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

		lines := []*model.PostingLine{
			{Amount: "100.00"},
			nil,
		}

		err := store.CreateMany(ctx, lines)

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

	t.Run("InsertManyError", func(t *testing.T) {
		expectedErr := errors.New("database error")
		stub := &repositoryStub{
			InsertManyFunc: func(ctx context.Context, objects []storable.Storable) error {
				return expectedErr
			},
		}

		store := &postingLinesStore{logger: logger, repo: stub}
		lines := []*model.PostingLine{
			{Amount: "100.00"},
		}

		err := store.CreateMany(ctx, lines)

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

	t.Run("BalancedEntry", func(t *testing.T) {
		var insertedLines []storable.Storable
		stub := &repositoryStub{
			InsertManyFunc: func(ctx context.Context, objects []storable.Storable) error {
				insertedLines = objects
				return nil
			},
		}

		store := &postingLinesStore{logger: logger, repo: stub}
		entryRef := bson.NewObjectID()
		cashAccount := bson.NewObjectID()
		revenueAccount := bson.NewObjectID()

		lines := []*model.PostingLine{
			{
				JournalEntryRef: entryRef,
				AccountRef:      cashAccount,
				LineType:        model.LineTypeMain,
				Amount:          "500.00",
			},
			{
				JournalEntryRef: entryRef,
				AccountRef:      revenueAccount,
				LineType:        model.LineTypeMain,
				Amount:          "500.00",
			},
		}

		err := store.CreateMany(ctx, lines)

		require.NoError(t, err)
		assert.Len(t, insertedLines, 2)
	})
}

func TestPostingLinesStore_ListByJournalEntry(t *testing.T) {
	ctx := context.Background()
	logger := zap.NewNop()
	entryRef := bson.NewObjectID()

	t.Run("Success", func(t *testing.T) {
		called := false
		stub := &repositoryStub{
			FindManyByFilterFunc: func(ctx context.Context, _ builder.Query, decoder rd.DecodingFunc) error {
				called = true
				return nil
			},
		}

		store := &postingLinesStore{logger: logger, repo: stub}
		results, err := store.ListByJournalEntry(ctx, entryRef)

		require.NoError(t, err)
		assert.True(t, called)
		assert.NotNil(t, results)
	})

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

		results, err := store.ListByJournalEntry(ctx, bson.NilObjectID)

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

	t.Run("EmptyResult", func(t *testing.T) {
		stub := &repositoryStub{
			FindManyByFilterFunc: func(ctx context.Context, _ builder.Query, decoder rd.DecodingFunc) error {
				return nil
			},
		}

		store := &postingLinesStore{logger: logger, repo: stub}
		results, err := store.ListByJournalEntry(ctx, entryRef)

		require.NoError(t, err)
		assert.Len(t, results, 0)
	})

	t.Run("FindError", func(t *testing.T) {
		expectedErr := errors.New("database error")
		stub := &repositoryStub{
			FindManyByFilterFunc: func(ctx context.Context, _ builder.Query, decoder rd.DecodingFunc) error {
				return expectedErr
			},
		}

		store := &postingLinesStore{logger: logger, repo: stub}
		results, err := store.ListByJournalEntry(ctx, entryRef)

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

func TestPostingLinesStore_ListByAccount(t *testing.T) {
	ctx := context.Background()
	logger := zap.NewNop()
	accountRef := bson.NewObjectID()

	t.Run("Success", func(t *testing.T) {
		called := false
		stub := &repositoryStub{
			FindManyByFilterFunc: func(ctx context.Context, _ builder.Query, decoder rd.DecodingFunc) error {
				called = true
				return nil
			},
		}

		store := &postingLinesStore{logger: logger, repo: stub}
		results, err := store.ListByAccount(ctx, accountRef, 10, 0)

		require.NoError(t, err)
		assert.True(t, called)
		assert.NotNil(t, results)
	})

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

		results, err := store.ListByAccount(ctx, bson.NilObjectID, 10, 0)

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

	t.Run("WithPagination", func(t *testing.T) {
		called := false
		stub := &repositoryStub{
			FindManyByFilterFunc: func(ctx context.Context, _ builder.Query, decoder rd.DecodingFunc) error {
				called = true
				return nil
			},
		}

		store := &postingLinesStore{logger: logger, repo: stub}
		results, err := store.ListByAccount(ctx, accountRef, 2, 2)

		require.NoError(t, err)
		assert.True(t, called)
		assert.NotNil(t, results)
	})

	t.Run("EmptyResult", func(t *testing.T) {
		stub := &repositoryStub{
			FindManyByFilterFunc: func(ctx context.Context, _ builder.Query, decoder rd.DecodingFunc) error {
				return nil
			},
		}

		store := &postingLinesStore{logger: logger, repo: stub}
		results, err := store.ListByAccount(ctx, accountRef, 10, 0)

		require.NoError(t, err)
		assert.Len(t, results, 0)
	})

	t.Run("FindError", func(t *testing.T) {
		expectedErr := errors.New("database error")
		stub := &repositoryStub{
			FindManyByFilterFunc: func(ctx context.Context, _ builder.Query, decoder rd.DecodingFunc) error {
				return expectedErr
			},
		}

		store := &postingLinesStore{logger: logger, repo: stub}
		results, err := store.ListByAccount(ctx, accountRef, 10, 0)

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