package store

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

	"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"
	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.mongodb.org/mongo-driver/v2/mongo"
	"go.uber.org/zap"
)

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

	t.Run("Success", func(t *testing.T) {
		var insertedEntry *model.JournalEntry
		stub := &repositoryStub{
			InsertFunc: func(ctx context.Context, object storable.Storable, _ builder.Query) error {
				insertedEntry = object.(*model.JournalEntry)
				return nil
			},
		}

		store := &journalEntriesStore{logger: logger, repo: stub}
		entry := &model.JournalEntry{
			IdempotencyKey: "test-key-123",
			EventTime:      time.Now(),
			EntryType:      model.EntryTypeCredit,
			Description:    "Test invoice entry",
		}

		err := store.Create(ctx, entry)

		require.NoError(t, err)
		assert.NotNil(t, insertedEntry)
		assert.Equal(t, "test-key-123", insertedEntry.IdempotencyKey)
		assert.Equal(t, model.EntryTypeCredit, insertedEntry.EntryType)
	})

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

		err := store.Create(ctx, nil)

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

	t.Run("DuplicateIdempotencyKey", func(t *testing.T) {
		stub := &repositoryStub{
			InsertFunc: func(ctx context.Context, object storable.Storable, _ builder.Query) error {
				return mongo.WriteException{
					WriteErrors: []mongo.WriteError{
						{Code: 11000}, // Duplicate key error
					},
				}
			},
		}

		store := &journalEntriesStore{logger: logger, repo: stub}
		entry := &model.JournalEntry{
			IdempotencyKey: "duplicate-key",
			EventTime:      time.Now(),
		}

		err := store.Create(ctx, entry)

		require.Error(t, err)
		assert.True(t, errors.Is(err, storage.ErrDuplicateIdempotency))
	})

	t.Run("InsertError", func(t *testing.T) {
		expectedErr := errors.New("database error")
		stub := &repositoryStub{
			InsertFunc: func(ctx context.Context, object storable.Storable, _ builder.Query) error {
				return expectedErr
			},
		}

		store := &journalEntriesStore{logger: logger, repo: stub}
		entry := &model.JournalEntry{
			IdempotencyKey: "test-key",
			EventTime:      time.Now(),
		}

		err := store.Create(ctx, entry)

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

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

	t.Run("Success", func(t *testing.T) {
		entryRef := bson.NewObjectID()
		stub := &repositoryStub{
			GetFunc: func(ctx context.Context, id bson.ObjectID, result storable.Storable) error {
				entry := result.(*model.JournalEntry)
				entry.SetID(entryRef)
				entry.IdempotencyKey = "test-key-123"
				entry.EntryType = model.EntryTypeDebit
				return nil
			},
		}

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

		require.NoError(t, err)
		assert.NotNil(t, result)
		assert.Equal(t, "test-key-123", result.IdempotencyKey)
		assert.Equal(t, model.EntryTypeDebit, result.EntryType)
	})

	t.Run("ZeroID", func(t *testing.T) {
		stub := &repositoryStub{}
		store := &journalEntriesStore{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) {
		entryRef := bson.NewObjectID()
		stub := &repositoryStub{
			GetFunc: func(ctx context.Context, id bson.ObjectID, result storable.Storable) error {
				return merrors.ErrNoData
			},
		}

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

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

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

	t.Run("Success", func(t *testing.T) {
		stub := &repositoryStub{
			FindOneByFilterFunc: func(ctx context.Context, _ builder.Query, result storable.Storable) error {
				entry := result.(*model.JournalEntry)
				entry.IdempotencyKey = "unique-key-123"
				entry.EntryType = model.EntryTypeReverse
				return nil
			},
		}

		store := &journalEntriesStore{logger: logger, repo: stub}
		result, err := store.GetByIdempotencyKey(ctx, orgRef, "unique-key-123")

		require.NoError(t, err)
		assert.NotNil(t, result)
		assert.Equal(t, "unique-key-123", result.IdempotencyKey)
		assert.Equal(t, model.EntryTypeReverse, result.EntryType)
	})

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

		result, err := store.GetByIdempotencyKey(ctx, bson.NilObjectID, "test-key")

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

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

		result, err := store.GetByIdempotencyKey(ctx, orgRef, "")

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

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

		store := &journalEntriesStore{logger: logger, repo: stub}
		result, err := store.GetByIdempotencyKey(ctx, orgRef, "nonexistent-key")

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

func TestJournalEntriesStore_ListByOrganization(t *testing.T) {
	ctx := context.Background()
	logger := zap.NewNop()
	orgRef := 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 := &journalEntriesStore{logger: logger, repo: stub}
		results, err := store.ListByOrganization(ctx, orgRef, 10, 0)

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

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

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

		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 := &journalEntriesStore{logger: logger, repo: stub}
		results, err := store.ListByOrganization(ctx, orgRef, 10, 0)

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

	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 := &journalEntriesStore{logger: logger, repo: stub}
		results, err := store.ListByOrganization(ctx, orgRef, 2, 1)

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

	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 := &journalEntriesStore{logger: logger, repo: stub}
		results, err := store.ListByOrganization(ctx, orgRef, 10, 0)

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