package ledger

import (
	"testing"
	"time"

	"github.com/shopspring/decimal"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/tech/sendico/ledger/storage/model"
	moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
	ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
	"go.mongodb.org/mongo-driver/v2/bson"
	"google.golang.org/protobuf/types/known/timestamppb"
)

func TestParseObjectID(t *testing.T) {
	t.Run("ValidObjectID", func(t *testing.T) {
		validID := bson.NewObjectID()
		result, err := parseObjectID(validID.Hex())

		require.NoError(t, err)
		assert.Equal(t, validID, result)
	})

	t.Run("EmptyString", func(t *testing.T) {
		result, err := parseObjectID("")

		require.Error(t, err)
		assert.Equal(t, bson.NilObjectID, result)
		assert.Contains(t, err.Error(), "empty object ID")
	})

	t.Run("InvalidHexString", func(t *testing.T) {
		result, err := parseObjectID("invalid-hex-string")

		require.Error(t, err)
		assert.Equal(t, bson.NilObjectID, result)
		assert.Contains(t, err.Error(), "invalid object ID")
	})

	t.Run("IncorrectLength", func(t *testing.T) {
		result, err := parseObjectID("abc123")

		require.Error(t, err)
		assert.Equal(t, bson.NilObjectID, result)
	})
}

func TestParseDecimal(t *testing.T) {
	t.Run("ValidDecimal", func(t *testing.T) {
		result, err := parseDecimal("123.45")

		require.NoError(t, err)
		assert.True(t, result.Equal(decimal.NewFromFloat(123.45)))
	})

	t.Run("EmptyString", func(t *testing.T) {
		result, err := parseDecimal("")

		require.Error(t, err)
		assert.True(t, result.IsZero())
		assert.Contains(t, err.Error(), "empty amount")
	})

	t.Run("InvalidDecimal", func(t *testing.T) {
		result, err := parseDecimal("not-a-number")

		require.Error(t, err)
		assert.True(t, result.IsZero())
		assert.Contains(t, err.Error(), "invalid decimal amount")
	})

	t.Run("NegativeDecimal", func(t *testing.T) {
		result, err := parseDecimal("-100.50")

		require.NoError(t, err)
		assert.True(t, result.Equal(decimal.NewFromFloat(-100.50)))
	})

	t.Run("ZeroDecimal", func(t *testing.T) {
		result, err := parseDecimal("0")

		require.NoError(t, err)
		assert.True(t, result.IsZero())
	})
}

func TestValidateMoney(t *testing.T) {
	t.Run("ValidMoney", func(t *testing.T) {
		money := &moneyv1.Money{
			Amount:   "100.50",
			Currency: "USD",
		}

		err := validateMoney(money, "test_field")
		assert.NoError(t, err)
	})

	t.Run("NilMoney", func(t *testing.T) {
		err := validateMoney(nil, "test_field")

		require.Error(t, err)
		assert.Contains(t, err.Error(), "test_field: money is required")
	})

	t.Run("EmptyAmount", func(t *testing.T) {
		money := &moneyv1.Money{
			Amount:   "",
			Currency: "USD",
		}

		err := validateMoney(money, "test_field")

		require.Error(t, err)
		assert.Contains(t, err.Error(), "test_field: amount is required")
	})

	t.Run("EmptyCurrency", func(t *testing.T) {
		money := &moneyv1.Money{
			Amount:   "100.50",
			Currency: "",
		}

		err := validateMoney(money, "test_field")

		require.Error(t, err)
		assert.Contains(t, err.Error(), "test_field: currency is required")
	})

	t.Run("InvalidAmount", func(t *testing.T) {
		money := &moneyv1.Money{
			Amount:   "invalid",
			Currency: "USD",
		}

		err := validateMoney(money, "test_field")

		require.Error(t, err)
		assert.Contains(t, err.Error(), "invalid decimal amount")
	})
}

func TestValidatePostingLines(t *testing.T) {
	t.Run("ValidPostingLines", func(t *testing.T) {
		lines := []*ledgerv1.PostingLine{
			{
				LedgerAccountRef: bson.NewObjectID().Hex(),
				Money: &moneyv1.Money{
					Amount:   "10.00",
					Currency: "USD",
				},
				LineType: ledgerv1.LineType_LINE_FEE,
			},
		}

		err := validatePostingLines(lines)
		assert.NoError(t, err)
	})

	t.Run("EmptyLines", func(t *testing.T) {
		err := validatePostingLines([]*ledgerv1.PostingLine{})
		assert.NoError(t, err)
	})

	t.Run("NilLine", func(t *testing.T) {
		lines := []*ledgerv1.PostingLine{nil}

		err := validatePostingLines(lines)

		require.Error(t, err)
		assert.Contains(t, err.Error(), "nil posting line")
	})

	t.Run("EmptyAccountRef", func(t *testing.T) {
		lines := []*ledgerv1.PostingLine{
			{
				LedgerAccountRef: "",
				Money: &moneyv1.Money{
					Amount:   "10.00",
					Currency: "USD",
				},
			},
		}

		err := validatePostingLines(lines)

		require.Error(t, err)
		assert.Contains(t, err.Error(), "ledger_account_ref is required")
	})

	t.Run("NilMoney", func(t *testing.T) {
		lines := []*ledgerv1.PostingLine{
			{
				LedgerAccountRef: bson.NewObjectID().Hex(),
				Money:            nil,
			},
		}

		err := validatePostingLines(lines)

		require.Error(t, err)
		assert.Contains(t, err.Error(), "money is required")
	})

	t.Run("MainLineType", func(t *testing.T) {
		lines := []*ledgerv1.PostingLine{
			{
				LedgerAccountRef: bson.NewObjectID().Hex(),
				Money: &moneyv1.Money{
					Amount:   "10.00",
					Currency: "USD",
				},
				LineType: ledgerv1.LineType_LINE_MAIN,
			},
		}

		err := validatePostingLines(lines)

		require.Error(t, err)
		assert.Contains(t, err.Error(), "cannot have LINE_MAIN type")
	})
}

func TestGetEventTime(t *testing.T) {
	t.Run("ValidTimestamp", func(t *testing.T) {
		now := time.Now()
		ts := timestamppb.New(now)

		result := getEventTime(ts)

		assert.True(t, result.Sub(now) < time.Second)
	})

	t.Run("NilTimestamp", func(t *testing.T) {
		before := time.Now()
		result := getEventTime(nil)
		after := time.Now()

		assert.True(t, result.After(before) || result.Equal(before))
		assert.True(t, result.Before(after) || result.Equal(after))
	})

	t.Run("InvalidTimestamp", func(t *testing.T) {
		// Create an invalid timestamp with negative seconds
		ts := &timestamppb.Timestamp{Seconds: -1, Nanos: -1}

		// Invalid timestamp should return current time
		before := time.Now()
		result := getEventTime(ts)
		after := time.Now()

		// Result should be close to now since timestamp is invalid
		assert.True(t, result.After(before.Add(-time.Second)) || result.Equal(before))
		assert.True(t, result.Before(after.Add(time.Second)) || result.Equal(after))
	})
}

func TestProtoLineTypeToModel(t *testing.T) {
	tests := []struct {
		name     string
		input    ledgerv1.LineType
		expected model.LineType
	}{
		{"Main", ledgerv1.LineType_LINE_MAIN, model.LineTypeMain},
		{"Fee", ledgerv1.LineType_LINE_FEE, model.LineTypeFee},
		{"Spread", ledgerv1.LineType_LINE_SPREAD, model.LineTypeSpread},
		{"Reversal", ledgerv1.LineType_LINE_REVERSAL, model.LineTypeReversal},
		{"Unspecified", ledgerv1.LineType_LINE_TYPE_UNSPECIFIED, model.LineTypeMain},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := protoLineTypeToModel(tt.input)
			assert.Equal(t, tt.expected, result)
		})
	}
}

func TestModelLineTypeToProto(t *testing.T) {
	tests := []struct {
		name     string
		input    model.LineType
		expected ledgerv1.LineType
	}{
		{"Main", model.LineTypeMain, ledgerv1.LineType_LINE_MAIN},
		{"Fee", model.LineTypeFee, ledgerv1.LineType_LINE_FEE},
		{"Spread", model.LineTypeSpread, ledgerv1.LineType_LINE_SPREAD},
		{"Reversal", model.LineTypeReversal, ledgerv1.LineType_LINE_REVERSAL},
		{"Unknown", model.LineType("unknown"), ledgerv1.LineType_LINE_TYPE_UNSPECIFIED},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := modelLineTypeToProto(tt.input)
			assert.Equal(t, tt.expected, result)
		})
	}
}

func TestModelEntryTypeToProto(t *testing.T) {
	tests := []struct {
		name     string
		input    model.EntryType
		expected ledgerv1.EntryType
	}{
		{"Credit", model.EntryTypeCredit, ledgerv1.EntryType_ENTRY_CREDIT},
		{"Debit", model.EntryTypeDebit, ledgerv1.EntryType_ENTRY_DEBIT},
		{"Transfer", model.EntryTypeTransfer, ledgerv1.EntryType_ENTRY_TRANSFER},
		{"FX", model.EntryTypeFX, ledgerv1.EntryType_ENTRY_FX},
		{"Fee", model.EntryTypeFee, ledgerv1.EntryType_ENTRY_FEE},
		{"Adjust", model.EntryTypeAdjust, ledgerv1.EntryType_ENTRY_ADJUST},
		{"Reverse", model.EntryTypeReverse, ledgerv1.EntryType_ENTRY_REVERSE},
		{"Unknown", model.EntryType("unknown"), ledgerv1.EntryType_ENTRY_TYPE_UNSPECIFIED},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			result := modelEntryTypeToProto(tt.input)
			assert.Equal(t, tt.expected, result)
		})
	}
}

func TestCalculateBalance(t *testing.T) {
	t.Run("PositiveBalance", func(t *testing.T) {
		lines := []*model.PostingLine{
			{Amount: "100.00"},
			{Amount: "50.00"},
		}

		result, err := calculateBalance(lines)

		require.NoError(t, err)
		assert.True(t, result.Equal(decimal.NewFromFloat(150.00)))
	})

	t.Run("NegativeBalance", func(t *testing.T) {
		lines := []*model.PostingLine{
			{Amount: "-100.00"},
			{Amount: "-50.00"},
		}

		result, err := calculateBalance(lines)

		require.NoError(t, err)
		assert.True(t, result.Equal(decimal.NewFromFloat(-150.00)))
	})

	t.Run("ZeroBalance", func(t *testing.T) {
		lines := []*model.PostingLine{
			{Amount: "100.00"},
			{Amount: "-100.00"},
		}

		result, err := calculateBalance(lines)

		require.NoError(t, err)
		assert.True(t, result.IsZero())
	})

	t.Run("EmptyLines", func(t *testing.T) {
		result, err := calculateBalance([]*model.PostingLine{})

		require.NoError(t, err)
		assert.True(t, result.IsZero())
	})

	t.Run("InvalidAmount", func(t *testing.T) {
		lines := []*model.PostingLine{
			{Amount: "invalid"},
		}

		_, err := calculateBalance(lines)
		require.Error(t, err)
	})
}

func TestValidateBalanced(t *testing.T) {
	t.Run("BalancedEntry", func(t *testing.T) {
		lines := []*model.PostingLine{
			{Amount: "100.00"},  // credit
			{Amount: "-100.00"}, // debit
		}

		err := validateBalanced(lines)
		assert.NoError(t, err)
	})

	t.Run("BalancedWithMultipleLines", func(t *testing.T) {
		lines := []*model.PostingLine{
			{Amount: "100.00"}, // credit
			{Amount: "-50.00"}, // debit
			{Amount: "-50.00"}, // debit
		}

		err := validateBalanced(lines)
		assert.NoError(t, err)
	})

	t.Run("UnbalancedEntry", func(t *testing.T) {
		lines := []*model.PostingLine{
			{Amount: "100.00"},
			{Amount: "-50.00"},
		}

		err := validateBalanced(lines)

		require.Error(t, err)
		assert.Contains(t, err.Error(), "must balance")
	})

	t.Run("EmptyLines", func(t *testing.T) {
		err := validateBalanced([]*model.PostingLine{})
		assert.NoError(t, err)
	})
}
