package store

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

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

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

		store := &outboxStore{logger: logger, repo: stub}
		event := &model.OutboxEvent{
			EventID: "evt_12345",
			Subject: "ledger.entry.created",
			Payload: []byte(`{"entryId":"123"}`),
			Status:  model.OutboxStatusPending,
		}

		err := store.Create(ctx, event)

		require.NoError(t, err)
		assert.NotNil(t, insertedEvent)
		assert.Equal(t, "evt_12345", insertedEvent.EventID)
		assert.Equal(t, "ledger.entry.created", insertedEvent.Subject)
		assert.Equal(t, model.OutboxStatusPending, insertedEvent.Status)
	})

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

		err := store.Create(ctx, nil)

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

	t.Run("DuplicateEventID", 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 := &outboxStore{logger: logger, repo: stub}
		event := &model.OutboxEvent{
			EventID: "duplicate_event",
			Subject: "test.subject",
			Status:  model.OutboxStatusPending,
		}

		err := store.Create(ctx, event)

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

	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 := &outboxStore{logger: logger, repo: stub}
		event := &model.OutboxEvent{
			EventID: "evt_123",
			Subject: "test.subject",
		}

		err := store.Create(ctx, event)

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

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

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

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

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

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

		store := &outboxStore{logger: logger, repo: stub}
		results, err := store.ListPending(ctx, 3)

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

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

func TestOutboxStore_MarkSent(t *testing.T) {
	ctx := context.Background()
	logger := zap.NewNop()
	eventRef := bson.NewObjectID()
	sentTime := time.Now()

	t.Run("Success", func(t *testing.T) {
		var patchedID bson.ObjectID
		stub := &repositoryStub{
			PatchFunc: func(ctx context.Context, id bson.ObjectID, _ repository.PatchDoc) error {
				patchedID = id
				return nil
			},
		}

		store := &outboxStore{logger: logger, repo: stub}
		err := store.MarkSent(ctx, eventRef, sentTime)

		require.NoError(t, err)
		assert.Equal(t, eventRef, patchedID)
	})

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

		err := store.MarkSent(ctx, bson.NilObjectID, sentTime)

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

	t.Run("PatchError", func(t *testing.T) {
		expectedErr := errors.New("database error")
		stub := &repositoryStub{
			PatchFunc: func(ctx context.Context, id bson.ObjectID, _ repository.PatchDoc) error {
				return expectedErr
			},
		}

		store := &outboxStore{logger: logger, repo: stub}
		err := store.MarkSent(ctx, eventRef, sentTime)

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

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

	t.Run("Success", func(t *testing.T) {
		var patchedID bson.ObjectID
		stub := &repositoryStub{
			PatchFunc: func(ctx context.Context, id bson.ObjectID, _ repository.PatchDoc) error {
				patchedID = id
				return nil
			},
		}

		store := &outboxStore{logger: logger, repo: stub}
		err := store.MarkFailed(ctx, eventRef)

		require.NoError(t, err)
		assert.Equal(t, eventRef, patchedID)
	})

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

		err := store.MarkFailed(ctx, bson.NilObjectID)

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

	t.Run("PatchError", func(t *testing.T) {
		expectedErr := errors.New("database error")
		stub := &repositoryStub{
			PatchFunc: func(ctx context.Context, id bson.ObjectID, _ repository.PatchDoc) error {
				return expectedErr
			},
		}

		store := &outboxStore{logger: logger, repo: stub}
		err := store.MarkFailed(ctx, eventRef)

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

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

	t.Run("Success", func(t *testing.T) {
		var patchedID bson.ObjectID
		stub := &repositoryStub{
			PatchFunc: func(ctx context.Context, id bson.ObjectID, _ repository.PatchDoc) error {
				patchedID = id
				return nil
			},
		}

		store := &outboxStore{logger: logger, repo: stub}
		err := store.IncrementAttempts(ctx, eventRef)

		require.NoError(t, err)
		assert.Equal(t, eventRef, patchedID)
	})

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

		err := store.IncrementAttempts(ctx, bson.NilObjectID)

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

	t.Run("PatchError", func(t *testing.T) {
		expectedErr := errors.New("database error")
		stub := &repositoryStub{
			PatchFunc: func(ctx context.Context, id bson.ObjectID, _ repository.PatchDoc) error {
				return expectedErr
			},
		}

		store := &outboxStore{logger: logger, repo: stub}
		err := store.IncrementAttempts(ctx, eventRef)

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

	t.Run("MultipleIncrements", func(t *testing.T) {
		var callCount int
		stub := &repositoryStub{
			PatchFunc: func(ctx context.Context, id bson.ObjectID, _ repository.PatchDoc) error {
				callCount++
				return nil
			},
		}

		store := &outboxStore{logger: logger, repo: stub}

		// Simulate multiple retry attempts
		for i := 0; i < 3; i++ {
			err := store.IncrementAttempts(ctx, eventRef)
			require.NoError(t, err)
		}

		assert.Equal(t, 3, callCount)
	})
}
