package ledger

import (
	"context"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.uber.org/zap"

	"github.com/tech/sendico/ledger/storage"
	"github.com/tech/sendico/pkg/merrors"
	pmodel "github.com/tech/sendico/pkg/model"
	"github.com/tech/sendico/pkg/model/account_role"
	describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
	ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
)

type accountStoreStub struct {
	createErr           error
	createErrSettlement error
	created             []*pmodel.LedgerAccount
	existing            *pmodel.LedgerAccount
	existingErr         error
	existingByRole      map[account_role.AccountRole]*pmodel.LedgerAccount
	defaultSettlement   *pmodel.LedgerAccount
	defaultErr          error
	createErrs          []error
}

func (s *accountStoreStub) Create(_ context.Context, account *pmodel.LedgerAccount) error {
	if account.Role == account_role.AccountRoleSettlement {
		if s.createErrSettlement != nil {
			return s.createErrSettlement
		}
	} else {
		if len(s.createErrs) > 0 {
			err := s.createErrs[0]
			s.createErrs = s.createErrs[1:]
			if err != nil {
				return err
			}
		} else if s.createErr != nil {
			return s.createErr
		}
	}

	if account.GetID() == nil || account.GetID().IsZero() {
		account.SetID(bson.NewObjectID())
	}

	account.CreatedAt = account.CreatedAt.UTC()
	account.UpdatedAt = account.UpdatedAt.UTC()

	s.created = append(s.created, account)
	return nil
}

func (s *accountStoreStub) GetByAccountCode(_ context.Context, _ bson.ObjectID, _ string, _ string) (*pmodel.LedgerAccount, error) {
	if s.existingErr != nil {
		return nil, s.existingErr
	}
	return s.existing, nil
}

func (s *accountStoreStub) Get(context.Context, bson.ObjectID) (*pmodel.LedgerAccount, error) {
	return nil, storage.ErrAccountNotFound
}

func (s *accountStoreStub) GetByRole(_ context.Context, orgRef bson.ObjectID, currency string, role account_role.AccountRole) (*pmodel.LedgerAccount, error) {
	if s.existingByRole != nil {
		if acc, ok := s.existingByRole[role]; ok {
			return acc, nil
		}
	}
	for _, acc := range s.created {
		if *acc.OrganizationRef == orgRef && acc.Currency == currency && acc.Role == role {
			return acc, nil
		}
	}
	return nil, storage.ErrAccountNotFound
}

func (s *accountStoreStub) GetSystemAccount(context.Context, pmodel.SystemAccountPurpose, string) (*pmodel.LedgerAccount, error) {
	return nil, storage.ErrAccountNotFound
}

func (s *accountStoreStub) GetDefaultSettlement(context.Context, bson.ObjectID, string) (*pmodel.LedgerAccount, error) {
	if s.defaultErr != nil {
		return nil, s.defaultErr
	}
	if s.defaultSettlement != nil {
		return s.defaultSettlement, nil
	}
	return nil, storage.ErrAccountNotFound
}

func (s *accountStoreStub) ListByOrganization(context.Context, bson.ObjectID, *storage.AccountsFilter, int, int) ([]*pmodel.LedgerAccount, error) {
	return []*pmodel.LedgerAccount{}, nil
}

func (s *accountStoreStub) UpdateStatus(context.Context, bson.ObjectID, pmodel.LedgerAccountStatus) error {
	return nil
}

type repositoryStub struct {
	accounts storage.AccountsStore
}

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

func TestCreateAccountResponder_Success(t *testing.T) {
	t.Parallel()

	orgRef := bson.NewObjectID()

	accountStore := &accountStoreStub{}
	svc := &Service{
		logger:  zap.NewNop(),
		storage: &repositoryStub{accounts: accountStore},
	}

	req := &ledgerv1.CreateAccountRequest{
		OrganizationRef: orgRef.Hex(),
		AccountType:     ledgerv1.AccountType_ACCOUNT_TYPE_ASSET,
		Currency:        "usd",
		AllowNegative:   false,
		Role:            ledgerv1.AccountRole_ACCOUNT_ROLE_FEE,
		Metadata:        map[string]string{"purpose": "primary"},
	}

	resp, err := svc.createAccountResponder(context.Background(), req)(context.Background())
	require.NoError(t, err)
	require.NotNil(t, resp)
	require.NotNil(t, resp.Account)

	// accountCode must be: "{accountType}:{CURRENCY}:{_id}"
	require.NotEmpty(t, resp.Account.AccountCode)
	require.NotEmpty(t, resp.Account.LedgerAccountRef)

	parts := strings.Split(resp.Account.AccountCode, ":")
	require.Len(t, parts, 3)
	require.Equal(t, "asset", parts[0])
	require.Equal(t, "usd", parts[1])
	require.Len(t, parts[2], 24)
	require.Equal(t, resp.Account.LedgerAccountRef, parts[2])

	require.Equal(t, ledgerv1.AccountType_ACCOUNT_TYPE_ASSET, resp.Account.AccountType)
	require.Equal(t, "USD", resp.Account.Currency)
	require.Equal(t, ledgerv1.AccountRole_ACCOUNT_ROLE_FEE, resp.Account.Role)
	require.Contains(t, resp.Account.Metadata, "purpose")

	// Typically: settlement + requested account
	require.Len(t, accountStore.created, 2)
}

func TestCreateAccountResponder_AutoCreatesSettlementAccount(t *testing.T) {
	t.Parallel()

	orgRef := bson.NewObjectID()

	accountStore := &accountStoreStub{}
	svc := &Service{
		logger:  zap.NewNop(),
		storage: &repositoryStub{accounts: accountStore},
	}

	req := &ledgerv1.CreateAccountRequest{
		OrganizationRef: orgRef.Hex(),
		AccountType:     ledgerv1.AccountType_ACCOUNT_TYPE_LIABILITY,
		Currency:        "usd",
	}

	resp, err := svc.createAccountResponder(context.Background(), req)(context.Background())
	require.NoError(t, err)
	require.NotNil(t, resp)
	require.NotNil(t, resp.Account)

	// default role
	require.Equal(t, ledgerv1.AccountRole_ACCOUNT_ROLE_OPERATING, resp.Account.Role)
	require.Equal(t, "USD", resp.Account.Currency)
	require.Equal(t, ledgerv1.AccountType_ACCOUNT_TYPE_LIABILITY, resp.Account.AccountType)
	require.Equal(t, defaultLedgerAccountName, resp.Account.GetDescribable().GetName())

	// Expect: required topology roles + dedicated operating account
	require.Len(t, accountStore.created, 6)

	var settlement *pmodel.LedgerAccount
	var operating *pmodel.LedgerAccount
	var operatingCount int

	roles := make(map[account_role.AccountRole]bool)
	for _, acc := range accountStore.created {
		roles[acc.Role] = true

		if acc.Role == account_role.AccountRoleSettlement {
			settlement = acc
		}
		if acc.Role == account_role.AccountRoleOperating {
			operatingCount++
			operating = acc
		}

		// General format check for every created account
		require.NotEmpty(t, acc.AccountCode)
		cc := strings.Split(acc.AccountCode, ":")
		require.Len(t, cc, 3)
		require.Equal(t, "usd", cc[1])
		require.Equal(t, acc.GetID().Hex(), cc[2])
	}

	require.NotNil(t, settlement)
	require.NotNil(t, operating)
	require.Equal(t, 2, operatingCount)

	for _, role := range RequiredRolesV1 {
		require.True(t, roles[role])
	}

	// Responder returns the dedicated operating account created for this request.
	require.Equal(t, operating.AccountCode, resp.Account.AccountCode)
	require.Equal(t, operating.GetID().Hex(), resp.Account.LedgerAccountRef)

	// Settlement expectations: system, asset, no negative
	stParts := strings.Split(settlement.AccountCode, ":")
	require.Len(t, stParts, 3)
	require.Equal(t, "asset", stParts[0])
	require.Equal(t, "usd", stParts[1])
	require.Equal(t, settlement.GetID().Hex(), stParts[2])

	require.Equal(t, pmodel.LedgerAccountTypeAsset, settlement.AccountType)
	require.Equal(t, "USD", settlement.Currency)
	require.False(t, settlement.AllowNegative)
	require.Equal(t, account_role.AccountRoleSettlement, settlement.Role)
	require.Equal(t, "true", settlement.Metadata["system"])
}

func TestCreateAccountResponder_OperatingPreservesProvidedNameAndType(t *testing.T) {
	t.Parallel()

	orgRef := bson.NewObjectID()
	accountStore := &accountStoreStub{}
	svc := &Service{
		logger:  zap.NewNop(),
		storage: &repositoryStub{accounts: accountStore},
	}

	req := &ledgerv1.CreateAccountRequest{
		OrganizationRef: orgRef.Hex(),
		AccountType:     ledgerv1.AccountType_ACCOUNT_TYPE_REVENUE,
		Currency:        "usd",
		Role:            ledgerv1.AccountRole_ACCOUNT_ROLE_OPERATING,
		Describable: &describablev1.Describable{
			Name: "Incoming revenue",
		},
	}

	resp, err := svc.createAccountResponder(context.Background(), req)(context.Background())
	require.NoError(t, err)
	require.NotNil(t, resp)
	require.NotNil(t, resp.Account)

	require.Equal(t, ledgerv1.AccountType_ACCOUNT_TYPE_REVENUE, resp.Account.AccountType)
	require.Equal(t, "Incoming revenue", resp.Account.GetDescribable().GetName())

	// Topology accounts + dedicated operating account.
	require.Len(t, accountStore.created, 6)
}

func TestCreateAccountResponder_RetriesOnConflict(t *testing.T) {
	t.Parallel()

	orgRef := bson.NewObjectID()
	accountStore := &accountStoreStub{
		// first create attempt returns conflict, second succeeds
		createErrs: []error{merrors.DataConflict("duplicate")},
	}

	svc := &Service{
		logger:  zap.NewNop(),
		storage: &repositoryStub{accounts: accountStore},
	}

	req := &ledgerv1.CreateAccountRequest{
		OrganizationRef: orgRef.Hex(),
		AccountType:     ledgerv1.AccountType_ACCOUNT_TYPE_ASSET,
		Currency:        "usd",
		Role:            ledgerv1.AccountRole_ACCOUNT_ROLE_FEE,
	}

	resp, err := svc.createAccountResponder(context.Background(), req)(context.Background())
	require.NoError(t, err)
	require.NotNil(t, resp)
	require.NotNil(t, resp.Account)

	// settlement + fee
	require.Len(t, accountStore.created, 2)

	var createdFee *pmodel.LedgerAccount
	for _, acc := range accountStore.created {
		if acc.Role == account_role.AccountRoleFee {
			createdFee = acc
			break
		}
	}
	require.NotNil(t, createdFee)

	require.Equal(t, createdFee.AccountCode, resp.Account.AccountCode)
	require.Equal(t, createdFee.GetID().Hex(), resp.Account.LedgerAccountRef)

	parts := strings.Split(resp.Account.AccountCode, ":")
	require.Len(t, parts, 3)
	require.Equal(t, "asset", parts[0])
	require.Equal(t, "usd", parts[1])
	require.Equal(t, resp.Account.LedgerAccountRef, parts[2])
}

func TestCreateAccountResponder_InvalidAccountType(t *testing.T) {
	t.Parallel()

	svc := &Service{
		logger:  zap.NewNop(),
		storage: &repositoryStub{accounts: &accountStoreStub{}},
	}

	// AccountType missing => must fail
	req := &ledgerv1.CreateAccountRequest{
		OrganizationRef: bson.NewObjectID().Hex(),
		Currency:        "USD",
	}

	_, err := svc.createAccountResponder(context.Background(), req)(context.Background())
	require.Error(t, err)
}
