package wormhole_test

import (
	"bytes"
	"context"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/keys-pub/keys"

	"github.com/keys-pub/keys-ext/wormhole"
	"github.com/keys-pub/keys-ext/wormhole/sctp"
	"github.com/stretchr/testify/require"
)

// TODO: SCTP write buffer?
// TODO: Keep alive?
// TODO: Close, reconnect?
// TODO: Messages could have been omitted by network, include previous message ID

func TestNew(t *testing.T) {
	// wormhole.SetLogger(wormhole.NewLogger(wormhole.DebugLevel))
	// sctp.SetLogger(sctp.NewLogger(sctp.DebugLevel))
	env := testEnv(t)
	defer env.closeFn()

	alice := keys.NewEdX25519KeyFromSeed(keys.Bytes32(bytes.Repeat([]byte{0x01}, 32)))
	bob := keys.NewEdX25519KeyFromSeed(keys.Bytes32(bytes.Repeat([]byte{0x02}, 32)))
	vlta := wormhole.NewVault(alice)
	vltb := wormhole.NewVault(bob)
	testWormhole(t, env, true, alice, bob, vlta, vltb)
}

func TestWormholeSameKey(t *testing.T) {
	// wormhole.SetLogger(wormhole.NewLogger(wormhole.DebugLevel))
	// sctp.SetLogger(sctp.NewLogger(sctp.DebugLevel))
	env := testEnv(t)
	defer env.closeFn()

	alice := keys.NewEdX25519KeyFromSeed(keys.Bytes32(bytes.Repeat([]byte{0x01}, 32)))
	vlta := wormhole.NewVault(alice)

	testWormhole(t, env, true, alice, alice, vlta, vlta)
}

func testWormhole(t *testing.T, env *env, local bool, alice *keys.EdX25519Key, bob *keys.EdX25519Key, vlta wormhole.Vault, vltb wormhole.Vault) {
	ctx := context.TODO()

	openWg := &sync.WaitGroup{}
	openWg.Add(2)

	closeWg := &sync.WaitGroup{}
	closeWg.Add(2)

	server := env.httpServer.URL
	wha, err := wormhole.New(server, vlta)
	require.NoError(t, err)
	defer wha.Close()
	wha.SetClock(env.clock)
	wha.OnStatus(func(st wormhole.Status) {
		switch st {
		case wormhole.Connected:
			openWg.Done()
		case wormhole.Closed:
			closeWg.Done()
		}
	})

	var offer *sctp.Addr
	if local {
		o, err := wha.CreateLocalOffer(ctx, alice.ID(), bob.ID())
		require.NoError(t, err)
		offer = o
	} else {
		o, err := wha.CreateOffer(ctx, alice.ID(), bob.ID())
		require.NoError(t, err)
		offer = o
	}

	inviteCode, err := wha.CreateInvite(ctx, alice.ID(), bob.ID())
	require.NoError(t, err)

	go func() {
		if err := wha.Connect(ctx, alice.ID(), bob.ID(), offer); err != nil {
			panic(err)
		}
	}()

	whb, err := wormhole.New(server, vltb)
	require.NoError(t, err)
	defer whb.Close()
	whb.SetClock(env.clock)
	whb.OnStatus(func(st wormhole.Status) {
		switch st {
		case wormhole.Connected:
			openWg.Done()
		case wormhole.Closed:
			closeWg.Done()
		}
	})

	if inviteCode != "" {
		invite, err := whb.FindInviteCode(ctx, inviteCode)
		if err != nil {
			return
		}
		require.Equal(t, invite.Sender, alice.ID())
		require.Equal(t, invite.Recipient, bob.ID())
	}

	go func() {
		if err := whb.Listen(ctx, bob.ID(), alice.ID(), offer); err != nil {
			panic(err)
		}
	}()

	openWg.Wait()

	err = wha.Write(ctx, []byte("ping"))
	require.NoError(t, err)

	go func() {
		b, err := whb.Read(ctx)
		require.NoError(t, err)
		require.Equal(t, "ping", string(b))
		err = whb.Write(ctx, []byte("pong"))
		require.NoError(t, err)
	}()

	b, err := wha.Read(ctx)
	require.NoError(t, err)
	require.Equal(t, "pong", string(b))

	// Message
	id := wormhole.NewID()
	pending, err := wha.WriteMessage(ctx, id, []byte("ping"), wormhole.UTF8Content)
	require.NoError(t, err)
	require.Equal(t, wormhole.Pending, pending.Type)
	require.Equal(t, id, pending.ID)

	msg, err := whb.ReadMessage(ctx, true)
	require.NoError(t, err)
	require.Equal(t, "ping", string(msg.Content.Data))
	require.Equal(t, id, string(msg.ID))

	reply, err := wha.ReadMessage(ctx, true)
	require.NoError(t, err)
	require.Equal(t, wormhole.Ack, reply.Type)
	require.Equal(t, id, reply.ID)

	wha.Close()

	_, err = whb.ReadMessage(ctx, true)
	require.EqualError(t, err, "closed")

	closeWg.Wait()
}

func TestWormholeCancel(t *testing.T) {
	// wormhole.SetLogger(wormhole.NewLogger(wormhole.DebugLevel))
	// sctp.SetLogger(sctp.NewLogger(sctp.DebugLevel))

	env := testEnv(t)
	defer env.closeFn()

	testWormholeCancel(t, env, 100*time.Millisecond)
	testWormholeCancel(t, env, time.Second)
	// testWormholeCancel(t, env, time.Second*5)
}

func testWormholeCancel(t *testing.T, env *env, dt time.Duration) {
	var err error
	server := env.httpServer.URL

	alice := keys.NewEdX25519KeyFromSeed(keys.Bytes32(bytes.Repeat([]byte{0x01}, 32)))
	bob := keys.NewEdX25519KeyFromSeed(keys.Bytes32(bytes.Repeat([]byte{0x02}, 32)))

	vlta := wormhole.NewVault(alice)

	wha, err := wormhole.New(server, vlta)
	require.NoError(t, err)
	defer wha.Close()
	wha.SetClock(env.clock)
	ctx, cancel := context.WithTimeout(context.Background(), dt)
	defer cancel()

	offer := &sctp.Addr{IP: "127.0.0.1", Port: 1234}
	err = wha.Listen(ctx, alice.ID(), bob.ID(), offer)
	require.True(t, strings.HasSuffix(err.Error(), "context deadline exceeded"))

	// TODO: Test cancel with Connect
}

func TestWormholeNoRecipient(t *testing.T) {
	// wormhole.SetLogger(wormhole.NewLogger(wormhole.DebugLevel))
	// sctp.SetLogger(sctp.NewLogger(sctp.DebugLevel))
	var err error
	env := testEnv(t)
	defer env.closeFn()
	server := env.httpServer.URL

	alice := keys.NewEdX25519KeyFromSeed(keys.Bytes32(bytes.Repeat([]byte{0x01}, 32)))
	bob := keys.NewEdX25519KeyFromSeed(keys.Bytes32(bytes.Repeat([]byte{0x02}, 32)))

	vlta := wormhole.NewVault(alice)
	vltb := wormhole.NewVault(bob)

	wha, err := wormhole.New(server, vlta)
	require.NoError(t, err)
	defer wha.Close()
	wha.SetClock(env.clock)

	whb, err := wormhole.New(server, vltb)
	require.NoError(t, err)
	defer wha.Close()
	whb.SetClock(env.clock)

	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
	defer cancel()

	offer, err := wha.CreateOffer(ctx, alice.ID(), bob.ID())
	require.NoError(t, err)
	// Don't Connect

	err = whb.Listen(ctx, alice.ID(), bob.ID(), offer)
	require.EqualError(t, err, "kex132yw8ht5p8cetl2jmvknewjawt9xwzdlrk2pyxlnwjyqrdq0dawqqph077 not found")

	wha.Close()
}
