package node

import (
	"bytes"
	"context"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"

	"github.com/DeBrosOfficial/network/pkg/config"
	"github.com/libp2p/go-libp2p"
	"github.com/libp2p/go-libp2p/core/crypto"
	"github.com/libp2p/go-libp2p/core/network"
	"github.com/libp2p/go-libp2p/core/peer"
	"github.com/multiformats/go-multiaddr"
)

func TestCalculateNextBackoff(t *testing.T) {
	if got := calculateNextBackoff(10 * time.Second); got <= 10*time.Second || got > 15*time.Second {
		t.Fatalf("unexpected next: %v", got)
	}
	if got := calculateNextBackoff(10 * time.Minute); got != 10*time.Minute {
		t.Fatalf("cap not applied: %v", got)
	}
}

func TestAddJitter(t *testing.T) {
	base := 10 * time.Second
	min := base - time.Duration(0.2*float64(base))
	max := base + time.Duration(0.2*float64(base))
	for i := 0; i < 100; i++ {
		got := addJitter(base)
		if got < time.Second || got < min || got > max {
			t.Fatalf("jitter out of range: %v", got)
		}
	}
}

func TestGetPeerId_WhenNoHost(t *testing.T) {
	n := &Node{}
	if id := n.GetPeerID(); id != "" {
		t.Fatalf("GetPeerID() = %q; want empty string when host is nil", id)
	}
}

func TestLoadOrCreateIdentity(t *testing.T) {
	t.Run("first run creates file with correct perms and round-trips", func(t *testing.T) {
		tempDir := t.TempDir()
		cfg := &config.Config{}
		cfg.Node.DataDir = tempDir

		n, err := NewNode(cfg)
		if err != nil {
			t.Fatalf("NewNode() error: %v", err)
		}

		priv, err := n.loadOrCreateIdentity()
		if err != nil {
			t.Fatalf("loadOrCreateIdentity() error: %v", err)
		}
		if priv == nil {
			t.Fatalf("returned private key is nil")
		}

		identityFile := filepath.Join(tempDir, "identity.key")

		// File exists
		fi, err := os.Stat(identityFile)
		if err != nil {
			t.Fatalf("identity file not created: %v", err)
		}

		// Permissions are 0600
		if got := fi.Mode().Perm(); got != 0o600 {
			t.Fatalf("identity file permissions are incorrect: %v", got)
		}

		// Saved key can be unmarshaled and matches returned key
		data, err := os.ReadFile(identityFile)
		if err != nil {
			t.Fatalf("failed to read identity file: %v", err)
		}
		priv2, err := crypto.UnmarshalPrivateKey(data)
		if err != nil {
			t.Fatalf("UnmarshalPrivateKey: %v", err)
		}

		b1, err := crypto.MarshalPrivateKey(priv)
		if err != nil {
			t.Fatalf("MarshalPrivateKey(priv): %v", err)
		}
		b2, err := crypto.MarshalPrivateKey(priv2)
		if err != nil {
			t.Fatalf("MarshalPrivateKey(priv2): %v", err)
		}
		if !bytes.Equal(b1, b2) {
			t.Fatalf("saved key differs from returned key")
		}
	})

	t.Run("second run returns same identity", func(t *testing.T) {
		tempDir := t.TempDir()
		cfg := &config.Config{}
		cfg.Node.DataDir = tempDir

		// First run
		n1, err := NewNode(cfg)
		if err != nil {
			t.Fatalf("NewNode() error: %v", err)
		}
		priv1, err := n1.loadOrCreateIdentity()
		if err != nil {
			t.Fatalf("loadOrCreateIdentity(first) error: %v", err)
		}

		// Second run with a new Node pointing to the same dir
		n2, err := NewNode(cfg)
		if err != nil {
			t.Fatalf("NewNode() error: %v", err)
		}
		priv2, err := n2.loadOrCreateIdentity()
		if err != nil {
			t.Fatalf("loadOrCreateIdentity(second) error: %v", err)
		}

		// Compare marshaled keys
		b1, err := crypto.MarshalPrivateKey(priv1)
		if err != nil {
			t.Fatalf("MarshalPrivateKey(priv1): %v", err)
		}
		b2, err := crypto.MarshalPrivateKey(priv2)
		if err != nil {
			t.Fatalf("MarshalPrivateKey(priv2): %v", err)
		}
		if !bytes.Equal(b1, b2) {
			t.Fatalf("second run did not return the same identity")
		}
	})
}

func TestHasPeerConnections(t *testing.T) {
	cfg := &config.Config{}

	n, err := NewNode(cfg)
	if err != nil {
		t.Fatalf("NewNode() error: %v", err)
	}

	// Assert: Does not have peer connections
	conns := n.hasPeerConnections()
	if conns != false {
		t.Fatalf("expected false, got %v", conns)
	}

	// Variation: Fresh libp2p still zero connections
	h, err := libp2p.New()
	if err != nil {
		t.Fatalf("libp2p.New() error: %v", err)
	}
	defer h.Close()

	n.host = h
	conns = n.hasPeerConnections()
	if conns != false {
		t.Fatalf("expected false, got %v", conns)
	}

	// Assert: Return true if connected to at least one peer
	t.Run("returns true when connected to at least one configured peer", func(t *testing.T) {
		// Fresh node and config
		cfg := &config.Config{}
		n2, err := NewNode(cfg)
		if err != nil {
			t.Fatalf("NewNode: %v", err)
		}

		// Create two hosts (A and B) listening on localhost TCP
		hA, err := libp2p.New(libp2p.ListenAddrStrings("/ip4/127.0.0.1/tcp/0"))
		if err != nil {
			t.Fatalf("libp2p.New (A): %v", err)
		}
		defer hA.Close()

		hB, err := libp2p.New(libp2p.ListenAddrStrings("/ip4/127.0.0.1/tcp/0"))
		if err != nil {
			t.Fatalf("libp2p.New (B): %v", err)
		}
		defer hB.Close()

		// Build B's peer multiaddr: <one-of-B.Addrs>/p2p/<B.ID>
		var base multiaddr.Multiaddr
		for _, a := range hB.Addrs() {
			if strings.Contains(a.String(), "/tcp/") {
				base = a
				break
			}
		}
		if base == nil {
			t.Skip("no TCP listen address for host B")
		}
		pidMA, err := multiaddr.NewMultiaddr("/p2p/" + hB.ID().String())
		if err != nil {
			t.Fatalf("NewMultiaddr(/p2p/<id>): %v", err)
		}
		peerAddr := base.Encapsulate(pidMA).String()

		// Configure node A with B as a peer
		n2.host = hA
		n2.config.Discovery.BootstrapPeers = []string{peerAddr}

		// Connect A -> B
		ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
		defer cancel()
		if err := hA.Connect(ctx, peer.AddrInfo{ID: hB.ID(), Addrs: hB.Addrs()}); err != nil {
			t.Fatalf("connect A->B: %v", err)
		}

		// Wait until connected
		deadline := time.Now().Add(2 * time.Second)
		for {
			if hA.Network().Connectedness(hB.ID()) == network.Connected {
				break
			}
			if time.Now().After(deadline) {
				t.Fatalf("A not connected to B after timeout")
			}
			time.Sleep(10 * time.Millisecond)
		}

		// Assert: hasPeerConnections returns true
		if !n2.hasPeerConnections() {
			t.Fatalf("expected hasPeerConnections() to be true")
		}
	})

	t.Run("returns false when connected peers are not in the peer list", func(t *testing.T) {
		// Fresh node and config
		cfg := &config.Config{}
		n2, err := NewNode(cfg)
		if err != nil {
			t.Fatalf("NewNode: %v", err)
		}

		// Create three hosts (A, B, C) listening on localhost TCP
		hA, err := libp2p.New(libp2p.ListenAddrStrings("/ip4/127.0.0.1/tcp/0"))
		if err != nil {
			t.Fatalf("libp2p.New (A): %v", err)
		}
		defer hA.Close()

		hB, err := libp2p.New(libp2p.ListenAddrStrings("/ip4/127.0.0.1/tcp/0"))
		if err != nil {
			t.Fatalf("libp2p.New (B): %v", err)
		}
		defer hB.Close()

		hC, err := libp2p.New(libp2p.ListenAddrStrings("/ip4/127.0.0.1/tcp/0"))
		if err != nil {
			t.Fatalf("libp2p.New (C): %v", err)
		}
		defer hC.Close()

		// Build C's peer multiaddr: <one-of-C.Addrs>/p2p/<C.ID>
		var baseC multiaddr.Multiaddr
		for _, a := range hC.Addrs() {
			if strings.Contains(a.String(), "/tcp/") {
				baseC = a
				break
			}
		}
		if baseC == nil {
			t.Skip("no TCP listen address for host C")
		}
		pidC, err := multiaddr.NewMultiaddr("/p2p/" + hC.ID().String())
		if err != nil {
			t.Fatalf("NewMultiaddr(/p2p/<id>): %v", err)
		}
		peerC := baseC.Encapsulate(pidC).String()

		// Configure node A with ONLY C as a peer
		n2.host = hA
		n2.config.Discovery.BootstrapPeers = []string{peerC}

		// Connect A -> B (but C is in the peer list, not B)
		ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
		defer cancel()
		if err := hA.Connect(ctx, peer.AddrInfo{ID: hB.ID(), Addrs: hB.Addrs()}); err != nil {
			t.Fatalf("connect A->B: %v", err)
		}

		// Wait until A is connected to B
		deadline := time.Now().Add(2 * time.Second)
		for {
			if hA.Network().Connectedness(hB.ID()) == network.Connected {
				break
			}
			if time.Now().After(deadline) {
				t.Fatalf("A not connected to B after timeout")
			}
			time.Sleep(10 * time.Millisecond)
		}

		// Assert: hasPeerConnections should be false (connected peer is not in peer list)
		if n2.hasPeerConnections() {
			t.Fatalf("expected hasPeerConnections() to be false")
		}
	})

}
