package ledger

import (
	"context"
	"encoding/json"
	"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/merrors"
	me "github.com/tech/sendico/pkg/messaging/envelope"
	pmodel "github.com/tech/sendico/pkg/model"
	"github.com/tech/sendico/pkg/model/account_role"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.uber.org/zap"
)

type stubRepository struct {
	accounts storage.AccountsStore
	balances storage.BalancesStore
	outbox   storage.OutboxStore
}

func (s *stubRepository) Ping(context.Context) error                  { return nil }
func (s *stubRepository) Accounts() storage.AccountsStore             { return s.accounts }
func (s *stubRepository) JournalEntries() storage.JournalEntriesStore { return nil }
func (s *stubRepository) PostingLines() storage.PostingLinesStore     { return nil }
func (s *stubRepository) Balances() storage.BalancesStore             { return s.balances }
func (s *stubRepository) Outbox() storage.OutboxStore                 { return s.outbox }

type stubAccountsStore struct {
	getByID           map[bson.ObjectID]*pmodel.LedgerAccount
	defaultSettlement *pmodel.LedgerAccount
	getErr            error
	defaultErr        error
}

func (s *stubAccountsStore) Create(context.Context, *pmodel.LedgerAccount) error {
	return merrors.NotImplemented("create")
}
func (s *stubAccountsStore) Get(ctx context.Context, accountRef bson.ObjectID) (*pmodel.LedgerAccount, error) {
	if s.getErr != nil {
		return nil, s.getErr
	}
	if acc, ok := s.getByID[accountRef]; ok {
		return acc, nil
	}
	return nil, storage.ErrAccountNotFound
}
func (s *stubAccountsStore) GetByAccountCode(context.Context, bson.ObjectID, string, string) (*pmodel.LedgerAccount, error) {
	return nil, merrors.NotImplemented("get by code")
}
func (s *stubAccountsStore) GetByRole(context.Context, bson.ObjectID, string, account_role.AccountRole) (*pmodel.LedgerAccount, error) {
	return nil, merrors.NotImplemented("get by role")
}
func (s *stubAccountsStore) GetSystemAccount(context.Context, pmodel.SystemAccountPurpose, string) (*pmodel.LedgerAccount, error) {
	return nil, merrors.NotImplemented("get system account")
}
func (s *stubAccountsStore) GetDefaultSettlement(context.Context, bson.ObjectID, string) (*pmodel.LedgerAccount, error) {
	if s.defaultErr != nil {
		return nil, s.defaultErr
	}
	if s.defaultSettlement == nil {
		return nil, storage.ErrAccountNotFound
	}
	return s.defaultSettlement, nil
}
func (s *stubAccountsStore) ListByOrganization(context.Context, bson.ObjectID, *storage.AccountsFilter, int, int) ([]*pmodel.LedgerAccount, error) {
	return nil, merrors.NotImplemented("list")
}
func (s *stubAccountsStore) UpdateStatus(context.Context, bson.ObjectID, pmodel.LedgerAccountStatus) error {
	return merrors.NotImplemented("update status")
}

type stubBalancesStore struct {
	records map[bson.ObjectID]*model.AccountBalance
	upserts []*model.AccountBalance
	getErr  error
	upErr   error
}

func (s *stubBalancesStore) Get(ctx context.Context, accountRef bson.ObjectID) (*model.AccountBalance, error) {
	if s.getErr != nil {
		return nil, s.getErr
	}
	if balance, ok := s.records[accountRef]; ok {
		return balance, nil
	}
	return nil, storage.ErrBalanceNotFound
}

func (s *stubBalancesStore) Upsert(ctx context.Context, balance *model.AccountBalance) error {
	if s.upErr != nil {
		return s.upErr
	}
	copied := *balance
	s.upserts = append(s.upserts, &copied)
	if s.records == nil {
		s.records = make(map[bson.ObjectID]*model.AccountBalance)
	}
	s.records[balance.AccountRef] = &copied
	return nil
}

func (s *stubBalancesStore) IncrementBalance(context.Context, bson.ObjectID, string) error {
	return merrors.NotImplemented("increment")
}

type stubOutboxStore struct {
	created []*model.OutboxEvent
	err     error
}

func (s *stubOutboxStore) Create(ctx context.Context, event *model.OutboxEvent) error {
	if s.err != nil {
		return s.err
	}
	copied := *event
	s.created = append(s.created, &copied)
	return nil
}

func (s *stubOutboxStore) ListPending(context.Context, int) ([]*model.OutboxEvent, error) {
	return nil, merrors.NotImplemented("list")
}

func (s *stubOutboxStore) MarkSent(context.Context, bson.ObjectID, time.Time) error {
	return merrors.NotImplemented("mark sent")
}

func (s *stubOutboxStore) MarkFailed(context.Context, bson.ObjectID) error {
	return merrors.NotImplemented("mark failed")
}

func (s *stubOutboxStore) IncrementAttempts(context.Context, bson.ObjectID) error {
	return merrors.NotImplemented("increment attempts")
}

func TestResolveSettlementAccount_Default(t *testing.T) {
	ctx := context.Background()
	orgRef := bson.NewObjectID()
	settlementID := bson.NewObjectID()
	settlement := &pmodel.LedgerAccount{}
	settlement.SetID(settlementID)
	settlement.OrganizationRef = &orgRef
	settlement.Currency = "USD"
	settlement.Status = pmodel.LedgerAccountStatusActive

	accounts := &stubAccountsStore{defaultSettlement: settlement}
	repo := &stubRepository{accounts: accounts}
	service := &Service{logger: zap.NewNop(), storage: repo}
	cache := make(map[bson.ObjectID]*pmodel.LedgerAccount)

	result, err := service.resolveSettlementAccount(ctx, orgRef, "USD", "", cache)

	require.NoError(t, err)
	assert.Equal(t, settlement, result)
	assert.Equal(t, settlement, cache[settlementID])
}

func TestResolveSettlementAccount_Override(t *testing.T) {
	ctx := context.Background()
	orgRef := bson.NewObjectID()
	overrideID := bson.NewObjectID()
	override := &pmodel.LedgerAccount{}
	override.SetID(overrideID)
	override.OrganizationRef = &orgRef
	override.Currency = "EUR"
	override.Status = pmodel.LedgerAccountStatusActive

	accounts := &stubAccountsStore{getByID: map[bson.ObjectID]*pmodel.LedgerAccount{overrideID: override}}
	repo := &stubRepository{accounts: accounts}
	service := &Service{logger: zap.NewNop(), storage: repo}
	cache := make(map[bson.ObjectID]*pmodel.LedgerAccount)

	result, err := service.resolveSettlementAccount(ctx, orgRef, "EUR", overrideID.Hex(), cache)

	require.NoError(t, err)
	assert.Equal(t, override, result)
	assert.Equal(t, override, cache[overrideID])
}

func TestResolveSettlementAccount_NoDefault(t *testing.T) {
	ctx := context.Background()
	orgRef := bson.NewObjectID()
	accounts := &stubAccountsStore{defaultErr: storage.ErrAccountNotFound}
	repo := &stubRepository{accounts: accounts}
	service := &Service{logger: zap.NewNop(), storage: repo}

	_, err := service.resolveSettlementAccount(ctx, orgRef, "USD", "", map[bson.ObjectID]*pmodel.LedgerAccount{})

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

func TestUpsertBalances_Succeeds(t *testing.T) {
	ctx := context.Background()
	orgRef := bson.NewObjectID()
	accountRef := bson.NewObjectID()
	account := &pmodel.LedgerAccount{AllowNegative: false, Currency: "USD"}
	account.OrganizationRef = &orgRef

	balanceLines := []*model.PostingLine{
		{
			AccountRef: accountRef,
			Amount:     "50",
			Currency:   "USD",
		},
	}

	balances := &stubBalancesStore{}
	repo := &stubRepository{balances: balances}
	service := &Service{logger: zap.NewNop(), storage: repo}
	accountCache := map[bson.ObjectID]*pmodel.LedgerAccount{accountRef: account}

	require.NoError(t, service.upsertBalances(ctx, balanceLines, accountCache))
	require.Len(t, balances.upserts, 1)
	assert.Equal(t, "50", balances.upserts[0].Balance)
	assert.Equal(t, int64(1), balances.upserts[0].Version)
	assert.Equal(t, "USD", balances.upserts[0].Currency)
}

func TestUpsertBalances_DisallowNegative(t *testing.T) {
	ctx := context.Background()
	orgRef := bson.NewObjectID()
	accountRef := bson.NewObjectID()
	account := &pmodel.LedgerAccount{AllowNegative: false, Currency: "USD"}
	account.OrganizationRef = &orgRef

	balanceLines := []*model.PostingLine{
		{
			AccountRef: accountRef,
			Amount:     "-10",
			Currency:   "USD",
		},
	}

	balances := &stubBalancesStore{}
	repo := &stubRepository{balances: balances}
	service := &Service{logger: zap.NewNop(), storage: repo}
	accountCache := map[bson.ObjectID]*pmodel.LedgerAccount{accountRef: account}

	err := service.upsertBalances(ctx, balanceLines, accountCache)

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

func TestEnqueueOutbox_CreatesEvent(t *testing.T) {
	ctx := context.Background()
	orgRef := bson.NewObjectID()
	entryID := bson.NewObjectID()
	entry := &model.JournalEntry{
		IdempotencyKey: "idem",
		EventTime:      time.Now().UTC(),
		EntryType:      model.EntryTypeCredit,
		Version:        42,
	}
	entry.OrganizationRef = orgRef
	entry.SetID(entryID)

	lines := []*model.PostingLine{
		{
			AccountRef: bson.NewObjectID(),
			Amount:     "100",
			Currency:   "USD",
			LineType:   model.LineTypeMain,
		},
	}

	producer := &stubOutboxStore{}
	repo := &stubRepository{outbox: producer}
	service := &Service{logger: zap.NewNop(), storage: repo}

	require.NoError(t, service.enqueueOutbox(ctx, entry, lines))
	require.Len(t, producer.created, 1)
	event := producer.created[0]
	assert.Equal(t, "ledger_outbox_sent", event.Subject)

	envelope, err := me.Deserialize(event.Payload)
	require.NoError(t, err)
	assert.Equal(t, outboxPublisherSender, envelope.GetSender())
	assert.Equal(t, "ledger_outbox_sent", envelope.GetSignature().ToString())

	var wrapped ledgerOutboxMessage
	require.NoError(t, json.Unmarshal(envelope.GetData(), &wrapped))
	assert.Equal(t, entryID.Hex(), wrapped.EventID)
	assert.Equal(t, ledgerOutboxSubject, wrapped.Subject)

	var payload outboxJournalPayload
	require.NoError(t, json.Unmarshal(wrapped.Payload, &payload))
	assert.Equal(t, entryID.Hex(), payload.JournalEntryRef)
	assert.Equal(t, "credit", payload.EntryType)
	assert.Len(t, payload.Lines, 1)
	assert.Equal(t, "100", payload.Lines[0].Amount)
}
