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/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"
	pkm "github.com/tech/sendico/pkg/model"
	"github.com/tech/sendico/pkg/model/account_role"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.mongodb.org/mongo-driver/v2/mongo"
	"go.uber.org/zap"
)

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

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

		store := &accountsStore{logger: logger, repo: stub}
		account := &pkm.LedgerAccount{
			AccountCode:   "1000",
			Currency:      "USD",
			AccountType:   pkm.LedgerAccountTypeAsset,
			Status:        pkm.LedgerAccountStatusActive,
			AllowNegative: false,
		}

		err := store.Create(ctx, account)

		require.NoError(t, err)
		assert.NotNil(t, insertedAccount)
		assert.Equal(t, "1000", insertedAccount.AccountCode)
		assert.Equal(t, "USD", insertedAccount.Currency)
	})

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

		err := store.Create(ctx, nil)

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

	t.Run("DuplicateAccountCode", 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 := &accountsStore{logger: logger, repo: stub}
		account := &pkm.LedgerAccount{
			AccountCode: "1000",
			Currency:    "USD",
		}

		err := store.Create(ctx, account)

		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 := &accountsStore{logger: logger, repo: stub}
		account := &pkm.LedgerAccount{AccountCode: "1000", Currency: "USD"}

		err := store.Create(ctx, account)

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

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

	t.Run("Success", func(t *testing.T) {
		accountRef := bson.NewObjectID()
		stub := &repositoryStub{
			GetFunc: func(ctx context.Context, id bson.ObjectID, result storable.Storable) error {
				account := result.(*pkm.LedgerAccount)
				account.SetID(accountRef)
				account.AccountCode = "1000"
				account.Currency = "USD"
				return nil
			},
		}

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

		require.NoError(t, err)
		assert.NotNil(t, result)
		assert.Equal(t, "1000", result.AccountCode)
		assert.Equal(t, "USD", result.Currency)
	})

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

		store := &accountsStore{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.ErrAccountNotFound))
	})

	t.Run("GetError", func(t *testing.T) {
		accountRef := bson.NewObjectID()
		expectedErr := errors.New("database error")
		stub := &repositoryStub{
			GetFunc: func(ctx context.Context, id bson.ObjectID, result storable.Storable) error {
				return expectedErr
			},
		}

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

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

func TestAccountsStore_GetByAccountCode(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 {
				account := result.(*pkm.LedgerAccount)
				account.AccountCode = "1000"
				account.Currency = "USD"
				return nil
			},
		}

		store := &accountsStore{logger: logger, repo: stub}
		result, err := store.GetByAccountCode(ctx, orgRef, "1000", "USD")

		require.NoError(t, err)
		assert.NotNil(t, result)
		assert.Equal(t, "1000", result.AccountCode)
		assert.Equal(t, "USD", result.Currency)
	})

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

		result, err := store.GetByAccountCode(ctx, bson.NilObjectID, "1000", "USD")

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

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

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

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

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

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

		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 := &accountsStore{logger: logger, repo: stub}
		result, err := store.GetByAccountCode(ctx, orgRef, "9999", "USD")

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

func TestAccountsStore_GetByRole(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 {
				account := result.(*pkm.LedgerAccount)
				account.Currency = "USD"
				account.Role = account_role.AccountRoleOperating
				return nil
			},
		}

		store := &accountsStore{logger: logger, repo: stub}
		result, err := store.GetByRole(ctx, orgRef, "USD", account_role.AccountRoleOperating)

		require.NoError(t, err)
		assert.NotNil(t, result)
		assert.Equal(t, account_role.AccountRoleOperating, result.Role)
		assert.Equal(t, "USD", result.Currency)
	})

	t.Run("ZeroOrganizationID", func(t *testing.T) {
		store := &accountsStore{logger: logger, repo: &repositoryStub{}}
		result, err := store.GetByRole(ctx, bson.NilObjectID, "USD", account_role.AccountRoleOperating)

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

	t.Run("EmptyCurrency", func(t *testing.T) {
		store := &accountsStore{logger: logger, repo: &repositoryStub{}}
		result, err := store.GetByRole(ctx, orgRef, "", account_role.AccountRoleOperating)

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

	t.Run("EmptyRole", func(t *testing.T) {
		store := &accountsStore{logger: logger, repo: &repositoryStub{}}
		result, err := store.GetByRole(ctx, orgRef, "USD", "")

		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 := &accountsStore{logger: logger, repo: stub}
		result, err := store.GetByRole(ctx, orgRef, "USD", account_role.AccountRoleOperating)

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

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

		store := &accountsStore{logger: logger, repo: stub}
		result, err := store.GetByRole(ctx, orgRef, "USD", account_role.AccountRoleOperating)

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

func TestAccountsStore_GetDefaultSettlement(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 {
				account := result.(*pkm.LedgerAccount)
				account.SetID(bson.NewObjectID())
				account.Currency = "USD"
				account.Role = account_role.AccountRoleSettlement
				return nil
			},
		}

		store := &accountsStore{logger: logger, repo: stub}
		result, err := store.GetDefaultSettlement(ctx, orgRef, "USD")

		require.NoError(t, err)
		assert.NotNil(t, result)
		assert.Equal(t, account_role.AccountRoleSettlement, result.Role)
		assert.Equal(t, "USD", result.Currency)
	})

	t.Run("ZeroOrganizationID", func(t *testing.T) {
		store := &accountsStore{logger: logger, repo: &repositoryStub{}}
		result, err := store.GetDefaultSettlement(ctx, bson.NilObjectID, "USD")

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

	t.Run("EmptyCurrency", func(t *testing.T) {
		store := &accountsStore{logger: logger, repo: &repositoryStub{}}
		result, err := store.GetDefaultSettlement(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 := &accountsStore{logger: logger, repo: stub}
		result, err := store.GetDefaultSettlement(ctx, orgRef, "USD")

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

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

		store := &accountsStore{logger: logger, repo: stub}
		result, err := store.GetDefaultSettlement(ctx, orgRef, "USD")

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

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

	t.Run("Success", func(t *testing.T) {
		stub := &repositoryStub{
			FindOneByFilterFunc: func(ctx context.Context, _ builder.Query, result storable.Storable) error {
				account := result.(*pkm.LedgerAccount)
				account.Currency = "USD"
				purpose := pkm.SystemAccountPurposeExternalSource
				account.SystemPurpose = &purpose
				account.Scope = pkm.LedgerAccountScopeSystem
				return nil
			},
		}

		store := &accountsStore{logger: logger, repo: stub}
		result, err := store.GetSystemAccount(ctx, pkm.SystemAccountPurposeExternalSource, "USD")

		require.NoError(t, err)
		require.NotNil(t, result)
		require.Equal(t, pkm.LedgerAccountScopeSystem, result.Scope)
		require.NotNil(t, result.SystemPurpose)
		require.Equal(t, pkm.SystemAccountPurposeExternalSource, *result.SystemPurpose)
		require.Equal(t, "USD", result.Currency)
	})

	t.Run("EmptyPurpose", func(t *testing.T) {
		store := &accountsStore{logger: logger, repo: &repositoryStub{}}
		result, err := store.GetSystemAccount(ctx, "", "USD")

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

	t.Run("EmptyCurrency", func(t *testing.T) {
		store := &accountsStore{logger: logger, repo: &repositoryStub{}}
		result, err := store.GetSystemAccount(ctx, pkm.SystemAccountPurposeExternalSink, "")

		require.Error(t, err)
		require.Nil(t, result)
		require.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 := &accountsStore{logger: logger, repo: stub}
		result, err := store.GetSystemAccount(ctx, pkm.SystemAccountPurposeExternalSource, "USD")

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

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

		store := &accountsStore{logger: logger, repo: stub}
		result, err := store.GetSystemAccount(ctx, pkm.SystemAccountPurposeExternalSource, "USD")

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

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

	t.Run("Success", func(t *testing.T) {
		var calledWithQuery bool
		stub := &repositoryStub{
			FindManyByFilterFunc: func(ctx context.Context, _ builder.Query, decoder rd.DecodingFunc) error {
				calledWithQuery = true
				// In unit tests, we just verify the method is called correctly
				// Integration tests would test the actual iteration logic
				return nil
			},
		}

		store := &accountsStore{logger: logger, repo: stub}
		results, err := store.ListByOrganization(ctx, orgRef, nil, 10, 0)

		require.NoError(t, err)
		assert.True(t, calledWithQuery, "FindManyByFilter should have been called")
		assert.NotNil(t, results)
	})

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

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

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

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

	t.Run("Success", func(t *testing.T) {
		var patchedID bson.ObjectID
		var patchedStatus pkm.LedgerAccountStatus
		stub := &repositoryStub{
			PatchFunc: func(ctx context.Context, id bson.ObjectID, _ repository.PatchDoc) error {
				patchedID = id
				// In real test, we'd inspect patch builder but this is sufficient for stub
				patchedStatus = pkm.LedgerAccountStatusFrozen
				return nil
			},
		}

		store := &accountsStore{logger: logger, repo: stub}
		err := store.UpdateStatus(ctx, accountRef, pkm.LedgerAccountStatusFrozen)

		require.NoError(t, err)
		assert.Equal(t, accountRef, patchedID)
		assert.Equal(t, pkm.LedgerAccountStatusFrozen, patchedStatus)
	})

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

		err := store.UpdateStatus(ctx, bson.NilObjectID, pkm.LedgerAccountStatusFrozen)

		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 := &accountsStore{logger: logger, repo: stub}
		err := store.UpdateStatus(ctx, accountRef, pkm.LedgerAccountStatusFrozen)

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