package autonat

import (
	"context"
	"testing"
	"time"

	"github.com/libp2p/go-libp2p/core/event"
	"github.com/libp2p/go-libp2p/core/host"
	"github.com/libp2p/go-libp2p/core/network"
	"github.com/libp2p/go-libp2p/core/peer"
	"github.com/libp2p/go-libp2p/p2p/host/autonat/pb"
	bhost "github.com/libp2p/go-libp2p/p2p/host/blank"
	swarmt "github.com/libp2p/go-libp2p/p2p/net/swarm/testing"

	"github.com/libp2p/go-msgio/pbio"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// these are mock service implementations for testing
func makeAutoNATServicePrivate(t *testing.T) host.Host {
	h := bhost.NewBlankHost(swarmt.GenSwarm(t))
	h.SetStreamHandler(AutoNATProto, sayPrivateStreamHandler(t))
	return h
}

func sayPrivateStreamHandler(t *testing.T) network.StreamHandler {
	return func(s network.Stream) {
		defer s.Close()
		r := pbio.NewDelimitedReader(s, network.MessageSizeMax)
		if err := r.ReadMsg(&pb.Message{}); err != nil {
			t.Error(err)
			return
		}
		w := pbio.NewDelimitedWriter(s)
		res := pb.Message{
			Type:         pb.Message_DIAL_RESPONSE.Enum(),
			DialResponse: newDialResponseError(pb.Message_E_DIAL_ERROR, "dial failed"),
		}
		w.WriteMsg(&res)
	}
}

func makeAutoNATRefuseDialRequest(t *testing.T, done chan struct{}) host.Host {
	h := bhost.NewBlankHost(swarmt.GenSwarm(t))
	h.SetStreamHandler(AutoNATProto, sayRefusedStreamHandler(t, done))
	return h
}

func sayRefusedStreamHandler(t *testing.T, done chan struct{}) network.StreamHandler {
	return func(s network.Stream) {
		defer s.Close()
		r := pbio.NewDelimitedReader(s, network.MessageSizeMax)
		if err := r.ReadMsg(&pb.Message{}); err != nil {
			// ignore error if the test has completed
			select {
			case _, ok := <-done:
				if !ok {
					return
				}
			default:
			}
			t.Error(err)
			return
		}
		w := pbio.NewDelimitedWriter(s)
		res := pb.Message{
			Type:         pb.Message_DIAL_RESPONSE.Enum(),
			DialResponse: newDialResponseError(pb.Message_E_DIAL_REFUSED, "dial refused"),
		}
		w.WriteMsg(&res)
	}
}

func makeAutoNATServicePublic(t *testing.T) host.Host {
	h := bhost.NewBlankHost(swarmt.GenSwarm(t))
	h.SetStreamHandler(AutoNATProto, func(s network.Stream) {
		defer s.Close()
		r := pbio.NewDelimitedReader(s, network.MessageSizeMax)
		if err := r.ReadMsg(&pb.Message{}); err != nil {
			t.Error(err)
			return
		}
		w := pbio.NewDelimitedWriter(s)
		res := pb.Message{
			Type:         pb.Message_DIAL_RESPONSE.Enum(),
			DialResponse: newDialResponseOK(s.Conn().RemoteMultiaddr()),
		}
		w.WriteMsg(&res)
	})
	return h
}

func makeAutoNAT(t *testing.T, ash host.Host) (host.Host, AutoNAT) {
	h := bhost.NewBlankHost(swarmt.GenSwarm(t))
	h.Peerstore().AddAddrs(ash.ID(), ash.Addrs(), time.Minute)
	h.Peerstore().AddProtocols(ash.ID(), AutoNATProto)
	a, _ := New(h, WithSchedule(100*time.Millisecond, time.Second), WithoutStartupDelay())
	a.(*AmbientAutoNAT).config.dialPolicy.allowSelfDials = true
	a.(*AmbientAutoNAT).config.throttlePeerPeriod = 100 * time.Millisecond
	return h, a
}

func identifyAsServer(server, recip host.Host) {
	recip.Peerstore().AddAddrs(server.ID(), server.Addrs(), time.Minute)
	recip.Peerstore().AddProtocols(server.ID(), AutoNATProto)

}

func connect(t *testing.T, a, b host.Host) {
	pinfo := peer.AddrInfo{ID: a.ID(), Addrs: a.Addrs()}
	err := b.Connect(context.Background(), pinfo)
	if err != nil {
		t.Fatal(err)
	}
}

func expectEvent(t *testing.T, s event.Subscription, expected network.Reachability, timeout time.Duration) {
	t.Helper()
	select {
	case e := <-s.Out():
		ev, ok := e.(event.EvtLocalReachabilityChanged)
		if !ok || ev.Reachability != expected {
			t.Fatal("got wrong event type from the bus")
		}

	case <-time.After(timeout):
		t.Fatal("failed to get the reachability event from the bus")
	}
}

// tests
func TestAutoNATPrivate(t *testing.T) {
	hs := makeAutoNATServicePrivate(t)
	defer hs.Close()
	hc, an := makeAutoNAT(t, hs)
	defer hc.Close()
	defer an.Close()

	// subscribe to AutoNat events
	s, err := hc.EventBus().Subscribe(&event.EvtLocalReachabilityChanged{})
	if err != nil {
		t.Fatalf("failed to subscribe to event EvtLocalReachabilityChanged, err=%s", err)
	}

	status := an.Status()
	if status != network.ReachabilityUnknown {
		t.Fatalf("unexpected NAT status: %d", status)
	}

	connect(t, hs, hc)
	expectEvent(t, s, network.ReachabilityPrivate, 3*time.Second)
}

func TestAutoNATPublic(t *testing.T) {
	hs := makeAutoNATServicePublic(t)
	defer hs.Close()
	hc, an := makeAutoNAT(t, hs)
	defer hc.Close()
	defer an.Close()

	// subscribe to AutoNat events
	s, err := hc.EventBus().Subscribe(&event.EvtLocalReachabilityChanged{})
	if err != nil {
		t.Fatalf("failed to subscribe to event EvtLocalReachabilityChanged, err=%s", err)
	}

	status := an.Status()
	if status != network.ReachabilityUnknown {
		t.Fatalf("unexpected NAT status: %d", status)
	}

	connect(t, hs, hc)
	expectEvent(t, s, network.ReachabilityPublic, 3*time.Second)
}

func TestAutoNATPublictoPrivate(t *testing.T) {
	hs := makeAutoNATServicePublic(t)
	defer hs.Close()
	hc, an := makeAutoNAT(t, hs)
	defer hc.Close()
	defer an.Close()

	// subscribe to AutoNat events
	s, err := hc.EventBus().Subscribe(&event.EvtLocalReachabilityChanged{})
	if err != nil {
		t.Fatalf("failed to subscribe to event EvtLocalReachabilityChanged, err=%s", err)
	}

	if status := an.Status(); status != network.ReachabilityUnknown {
		t.Fatalf("unexpected NAT status: %d", status)
	}

	connect(t, hs, hc)
	expectEvent(t, s, network.ReachabilityPublic, 3*time.Second)

	hs.SetStreamHandler(AutoNATProto, sayPrivateStreamHandler(t))
	hps := makeAutoNATServicePrivate(t)
	connect(t, hps, hc)
	identifyAsServer(hps, hc)

	expectEvent(t, s, network.ReachabilityPrivate, 3*time.Second)
}

func TestAutoNATIncomingEvents(t *testing.T) {
	hs := makeAutoNATServicePrivate(t)
	defer hs.Close()
	hc, ani := makeAutoNAT(t, hs)
	defer hc.Close()
	defer ani.Close()
	an := ani.(*AmbientAutoNAT)

	status := an.Status()
	if status != network.ReachabilityUnknown {
		t.Fatalf("unexpected NAT status: %d", status)
	}

	connect(t, hs, hc)

	em, _ := hc.EventBus().Emitter(&event.EvtPeerIdentificationCompleted{})
	em.Emit(event.EvtPeerIdentificationCompleted{Peer: hs.ID()})

	require.Eventually(t, func() bool {
		return an.Status() != network.ReachabilityUnknown
	}, 5*time.Second, 100*time.Millisecond, "Expected probe due to identification of autonat service")
}

func TestAutoNATDialRefused(t *testing.T) {
	hs := makeAutoNATServicePublic(t)
	defer hs.Close()
	hc, an := makeAutoNAT(t, hs)
	defer hc.Close()
	defer an.Close()

	// subscribe to AutoNat events
	s, err := hc.EventBus().Subscribe(&event.EvtLocalReachabilityChanged{})
	if err != nil {
		t.Fatalf("failed to subscribe to event EvtLocalReachabilityChanged, err=%s", err)
	}

	if status := an.Status(); status != network.ReachabilityUnknown {
		t.Fatalf("unexpected NAT status: %d", status)
	}

	connect(t, hs, hc)
	expectEvent(t, s, network.ReachabilityPublic, 10*time.Second)

	done := make(chan struct{})
	hs.SetStreamHandler(AutoNATProto, sayRefusedStreamHandler(t, done))
	hps := makeAutoNATRefuseDialRequest(t, done)
	connect(t, hps, hc)
	identifyAsServer(hps, hc)

	require.Never(t, func() bool {
		return an.Status() != network.ReachabilityPublic
	}, 3*time.Second, 1*time.Second, "Expected probe to not change reachability from public")
	close(done)
}

func recordObservation(an *AmbientAutoNAT, status network.Reachability) {
	an.observations <- status
}

func TestAutoNATObservationRecording(t *testing.T) {
	hs := makeAutoNATServicePublic(t)
	defer hs.Close()
	hc, ani := makeAutoNAT(t, hs)
	defer hc.Close()
	defer ani.Close()
	an := ani.(*AmbientAutoNAT)

	s, err := hc.EventBus().Subscribe(&event.EvtLocalReachabilityChanged{})
	if err != nil {
		t.Fatalf("failed to subscribe to event EvtLocalRoutabilityPublic, err=%s", err)
	}

	expectStatus := func(expected network.Reachability, msg string, args ...any) {
		require.EventuallyWithTf(t, func(collect *assert.CollectT) {
			assert.Equal(collect, expected, an.Status())
		}, 2*time.Second, 100*time.Millisecond, msg, args...)
	}

	recordObservation(an, network.ReachabilityPublic)
	expectStatus(network.ReachabilityPublic, "failed to transition to public.")
	expectEvent(t, s, network.ReachabilityPublic, 3*time.Second)

	// a single recording should have confidence still at 0, and transition to private quickly.
	recordObservation(an, network.ReachabilityPrivate)
	expectStatus(network.ReachabilityPrivate, "failed to transition to private.")

	expectEvent(t, s, network.ReachabilityPrivate, 3*time.Second)

	// stronger public confidence should be harder to undo.
	recordObservation(an, network.ReachabilityPublic)
	recordObservation(an, network.ReachabilityPublic)
	expectStatus(network.ReachabilityPublic, "failed to transition to public.")
	expectEvent(t, s, network.ReachabilityPublic, 3*time.Second)

	recordObservation(an, network.ReachabilityPrivate)
	expectStatus(network.ReachabilityPublic, "too-extreme private transition.")

	// Don't emit events if reachability hasn't changed
	recordObservation(an, network.ReachabilityPublic)
	expectStatus(network.ReachabilityPublic, "reachability should stay public")
	select {
	case <-s.Out():
		t.Fatal("received event without state transition")
	case <-time.After(300 * time.Millisecond):
	}
}

func TestStaticNat(t *testing.T) {
	_, cancel := context.WithCancel(context.Background())
	defer cancel()

	h := bhost.NewBlankHost(swarmt.GenSwarm(t))
	defer h.Close()
	s, _ := h.EventBus().Subscribe(&event.EvtLocalReachabilityChanged{})

	nat, err := New(h, WithReachability(network.ReachabilityPrivate))
	if err != nil {
		t.Fatal(err)
	}
	if nat.Status() != network.ReachabilityPrivate {
		t.Fatalf("should be private")
	}
	expectEvent(t, s, network.ReachabilityPrivate, 3*time.Second)
}
