package nip60

import (
	"bytes"
	"cmp"
	"context"
	"fmt"
	"slices"
	"testing"
	"time"

	"fiatjaf.com/nostr"
	"fiatjaf.com/nostr/keyer"
	"github.com/btcsuite/btcd/btcec/v2"
	"github.com/elnosh/gonuts/cashu"
	"github.com/stretchr/testify/require"
	"golang.org/x/exp/rand"
)

func TestWallet(t *testing.T) {
	ctx := context.Background()
	kr := keyer.NewPlainKeySigner(nostr.MustSecretKeyFromHex("040cbf11f24b080ad9d8669d7514d9f3b7b1f58e5a6dcb75549352b041656537"))

	privateKey, _ := btcec.NewPrivateKey()

	w := &Wallet{
		kr:         kr,
		PrivateKey: privateKey,
		PublicKey:  privateKey.PubKey(),
		Mints:      []string{"https://mint1.com", "https://mint2.com"},
		Tokens: []Token{
			{
				Mint:     "https://mint1.com",
				Proofs:   cashu.Proofs{{Amount: 100}},
				mintedAt: nostr.Timestamp(time.Now().Add(-3 * time.Hour).Unix()),
			},
			{
				Mint:     "https://mint2.com",
				Proofs:   cashu.Proofs{{Amount: 200}},
				mintedAt: nostr.Timestamp(time.Now().Add(-2 * time.Hour).Unix()),
			},
			{
				Mint:     "https://mint1.com",
				Proofs:   cashu.Proofs{{Amount: 300}},
				mintedAt: nostr.Timestamp(time.Now().Add(-1 * time.Hour).Unix()),
			},
		},
		History: []HistoryEntry{
			{
				In:        true,
				Amount:    100,
				createdAt: nostr.Timestamp(time.Now().Add(-3 * time.Hour).Unix()),
				TokenReferences: []TokenRef{
					{Created: true, EventID: nostr.MustIDFromHex("645babb9051f46ddc97d960e68f82934e627f136dde7b860bf87c9213d937b58")},
				},
			},
			{
				In:        true,
				Amount:    200,
				createdAt: nostr.Timestamp(time.Now().Add(-2 * time.Hour).Unix()),
				TokenReferences: []TokenRef{
					{Created: false, EventID: nostr.MustIDFromHex("add072ae7d7a027748e03024267a1c073f3fbc26cca468ba8630d039a7f5df72")},
					{Created: true, EventID: nostr.MustIDFromHex("b8460b5589b68a0d9a017ac3784d17a0729046206aa631f7f4b763b738e36cf8")},
				},
			},
			{
				In:        true,
				Amount:    300,
				createdAt: nostr.Timestamp(time.Now().Add(-1 * time.Hour).Unix()),
				TokenReferences: []TokenRef{
					{Created: false, EventID: nostr.MustIDFromHex("61f86031d0ab95e9134a3ab955e96104cb1f4d610172838d28aa7ae9dc1cc924")},
					{Created: true, EventID: nostr.MustIDFromHex("588b78e4af06e960434239e7367a0bedf84747d4c52ff943f5e8b7daa3e1b601"), IsNutzap: true},
					{Created: false, EventID: nostr.MustIDFromHex("8f14c0a4ff1bf85ccc26bf0125b9a289552f9b59bbb310b163d6a88a7bbd4ebc")},
					{Created: true, EventID: nostr.MustIDFromHex("41a6f442b7c3c9e2f1e8c4835c00f17c56b3e3be4c9f7cf7bc4cdd705b1b61db"), IsNutzap: true},
				},
			},
		},
	}

	// turn everything into events
	events := make([]nostr.Event, 0, 7)

	// wallet metadata event
	metaEvent := nostr.Event{}
	err := w.toEvent(ctx, kr, &metaEvent)
	require.NoError(t, err)
	events = append(events, metaEvent)

	// token events
	for i := range w.Tokens {
		evt := nostr.Event{}
		evt.Tags = nostr.Tags{}
		err := w.Tokens[i].toEvent(ctx, kr, &evt)
		require.NoError(t, err)
		w.Tokens[i].event = &evt
		events = append(events, evt)
	}

	// history events
	for i := range w.History {
		evt := nostr.Event{}
		evt.Tags = nostr.Tags{}
		err := w.History[i].toEvent(ctx, kr, &evt)
		require.NoError(t, err)
		w.History[i].event = &evt
		events = append(events, evt)
	}

	// test different orderings
	testCases := []struct {
		name string
		sort func([]nostr.Event)
	}{
		{
			name: "random order",
			sort: func(evts []nostr.Event) {
				r := rand.New(rand.NewSource(42)) // deterministic
				r.Shuffle(len(evts), func(i, j int) {
					evts[i], evts[j] = evts[j], evts[i]
				})
			},
		},
		{
			name: "most recent first",
			sort: func(evts []nostr.Event) {
				slices.SortFunc(evts, func(a, b nostr.Event) int {
					return int(b.CreatedAt - a.CreatedAt)
				})
			},
		},
		{
			name: "least recent first",
			sort: func(evts []nostr.Event) {
				slices.SortFunc(evts, func(a, b nostr.Event) int {
					return int(a.CreatedAt - b.CreatedAt)
				})
			},
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			// make a copy and sort it
			eventsCopy := make([]nostr.Event, len(events))
			copy(eventsCopy, events)
			tc.sort(eventsCopy)

			// create relay event channel
			evtChan := make(chan nostr.RelayEvent)
			eoseChan := make(chan struct{})

			// send events in a goroutine
			go func() {
				for _, evt := range eventsCopy {
					evtChan <- nostr.RelayEvent{Event: evt}
				}
				close(eoseChan)
				close(evtChan)
			}()

			// load wallet from events
			loaded := loadWallet(ctx, kr, evtChan, make(chan nostr.RelayEvent), eoseChan)
			loaded.Processed = func(evt nostr.Event, err error) {
				fmt.Println("processed", evt.Kind, err)
			}

			<-loaded.Stable

			require.Len(t, loaded.Tokens, len(w.Tokens))
			require.ElementsMatch(t, loaded.Tokens, w.Tokens)
			require.Len(t, loaded.History, len(w.History))
			slices.SortFunc(loaded.History, func(a, b HistoryEntry) int { return cmp.Compare(a.createdAt, b.createdAt) })
			slices.SortFunc(w.History, func(a, b HistoryEntry) int { return cmp.Compare(a.createdAt, b.createdAt) })
			for i := range w.History {
				slices.SortFunc(loaded.History[i].TokenReferences, func(a, b TokenRef) int { return bytes.Compare(a.EventID[:], b.EventID[:]) })
				slices.SortFunc(w.History[i].TokenReferences, func(a, b TokenRef) int { return bytes.Compare(a.EventID[:], b.EventID[:]) })
				require.Equal(t, loaded.History[i], w.History[i])
			}
			require.ElementsMatch(t, loaded.Mints, w.Mints)
			require.Equal(t, loaded.PublicKey.SerializeCompressed(), w.PublicKey.SerializeCompressed())
		})
	}
}
