package interfaces

import (
	"net"
	"testing"
	"time"

	"git.quad4.io/Networks/Reticulum-Go/pkg/common"
)

func TestNewAutoInterface(t *testing.T) {
	t.Run("DefaultConfig", func(t *testing.T) {
		config := &common.InterfaceConfig{Enabled: true}
		ai, err := NewAutoInterface("autoDefault", config)
		if err != nil {
			t.Fatalf("NewAutoInterface failed with default config: %v", err)
		}
		if ai == nil {
			t.Fatal("NewAutoInterface returned nil with default config")
		}

		if ai.GetName() != "autoDefault" {
			t.Errorf("GetName() = %s; want autoDefault", ai.GetName())
		}
		if ai.GetType() != common.IF_TYPE_AUTO {
			t.Errorf("GetType() = %v; want %v", ai.GetType(), common.IF_TYPE_AUTO)
		}
		if ai.discoveryPort != DEFAULT_DISCOVERY_PORT {
			t.Errorf("discoveryPort = %d; want %d", ai.discoveryPort, DEFAULT_DISCOVERY_PORT)
		}
		if ai.dataPort != DEFAULT_DATA_PORT {
			t.Errorf("dataPort = %d; want %d", ai.dataPort, DEFAULT_DATA_PORT)
		}
		if string(ai.groupID) != "reticulum" {
			t.Errorf("groupID = %s; want reticulum", string(ai.groupID))
		}
		if ai.discoveryScope != SCOPE_LINK {
			t.Errorf("discoveryScope = %s; want %s", ai.discoveryScope, SCOPE_LINK)
		}
		if len(ai.peers) != 0 {
			t.Errorf("peers map not empty initially")
		}
	})

	t.Run("CustomConfig", func(t *testing.T) {
		config := &common.InterfaceConfig{
			Enabled:       true,
			DiscoveryPort: 12345,
			DataPort:      54321,
			GroupID:       "customGroup",
		}
		ai, err := NewAutoInterface("autoCustom", config)
		if err != nil {
			t.Fatalf("NewAutoInterface failed with custom config: %v", err)
		}
		if ai == nil {
			t.Fatal("NewAutoInterface returned nil with custom config")
		}

		if ai.discoveryPort != 12345 {
			t.Errorf("discoveryPort = %d; want 12345", ai.discoveryPort)
		}
		if ai.dataPort != 54321 {
			t.Errorf("dataPort = %d; want 54321", ai.dataPort)
		}
		if string(ai.groupID) != "customGroup" {
			t.Errorf("groupID = %s; want customGroup", string(ai.groupID))
		}
	})
}

// mockAutoInterface embeds AutoInterface but overrides methods that start goroutines
type mockAutoInterface struct {
	*AutoInterface
}

func newMockAutoInterface(name string, config *common.InterfaceConfig) (*mockAutoInterface, error) {
	ai, err := NewAutoInterface(name, config)
	if err != nil {
		return nil, err
	}

	// Initialize maps that would normally be initialized in Start()
	ai.peers = make(map[string]*Peer)
	ai.linkLocalAddrs = make([]string, 0)
	ai.adoptedInterfaces = make(map[string]*AdoptedInterface)
	ai.interfaceServers = make(map[string]*net.UDPConn)
	ai.discoveryServers = make(map[string]*net.UDPConn)
	ai.multicastEchoes = make(map[string]time.Time)
	ai.timedOutInterfaces = make(map[string]time.Time)

	return &mockAutoInterface{AutoInterface: ai}, nil
}

func (m *mockAutoInterface) Start() error {
	// Don't start any goroutines
	return nil
}

func (m *mockAutoInterface) Stop() error {
	// Don't try to close connections that were never opened
	return nil
}

// mockHandlePeerAnnounce is a test-only method that doesn't handle its own locking
func (m *mockAutoInterface) mockHandlePeerAnnounce(addr *net.UDPAddr, ifaceName string) {
	peerAddr := addr.IP.String() + "%" + addr.Zone

	for _, localAddr := range m.linkLocalAddrs {
		if peerAddr == localAddr {
			m.multicastEchoes[ifaceName] = time.Now()
			return
		}
	}

	if _, exists := m.peers[peerAddr]; !exists {
		m.peers[peerAddr] = &Peer{
			ifaceName: ifaceName,
			lastHeard: time.Now(),
		}
	} else {
		m.peers[peerAddr].lastHeard = time.Now()
	}
}

func TestAutoInterfacePeerManagement(t *testing.T) {
	// Use a shorter timeout for testing
	testTimeout := 100 * time.Millisecond

	config := &common.InterfaceConfig{Enabled: true}
	ai, err := newMockAutoInterface("autoPeerTest", config)
	if err != nil {
		t.Fatalf("Failed to create mock interface: %v", err)
	}

	// Create a done channel to signal goroutine cleanup
	done := make(chan struct{})

	// Start peer management with done channel
	go func() {
		ticker := time.NewTicker(testTimeout)
		defer ticker.Stop()

		for {
			select {
			case <-ticker.C:
				ai.Mutex.Lock()
				now := time.Now()
				for addr, peer := range ai.peers {
					if now.Sub(peer.lastHeard) > testTimeout {
						delete(ai.peers, addr)
					}
				}
				ai.Mutex.Unlock()
			case <-done:
				return
			}
		}
	}()

	// Ensure cleanup
	defer func() {
		close(done)
		ai.Stop()
	}()

	// Simulate receiving peer announces
	peer1AddrStr := "fe80::1%eth0"
	peer2AddrStr := "fe80::2%eth0"
	localAddrStr := "fe80::aaaa%eth0" // Simulate a local address

	peer1Addr := &net.UDPAddr{IP: net.ParseIP("fe80::1"), Zone: "eth0"}
	peer2Addr := &net.UDPAddr{IP: net.ParseIP("fe80::2"), Zone: "eth0"}
	localAddr := &net.UDPAddr{IP: net.ParseIP("fe80::aaaa"), Zone: "eth0"}

	ai.Mutex.Lock()
	ai.linkLocalAddrs = append(ai.linkLocalAddrs, localAddrStr)
	ai.Mutex.Unlock()

	t.Run("AddPeer1", func(t *testing.T) {
		ai.Mutex.Lock()
		ai.mockHandlePeerAnnounce(peer1Addr, "eth0")
		ai.Mutex.Unlock()

		// Give a small amount of time for the peer to be processed
		time.Sleep(10 * time.Millisecond)

		ai.Mutex.RLock()
		count := len(ai.peers)
		peer, exists := ai.peers[peer1AddrStr]
		var ifaceName string
		if exists {
			ifaceName = peer.ifaceName
		}
		ai.Mutex.RUnlock()

		if count != 1 {
			t.Fatalf("Expected 1 peer, got %d", count)
		}
		if !exists {
			t.Fatalf("Peer %s not found in map", peer1AddrStr)
		}
		if ifaceName != "eth0" {
			t.Errorf("Peer %s interface name = %s; want eth0", peer1AddrStr, ifaceName)
		}
	})

	t.Run("AddPeer2", func(t *testing.T) {
		ai.Mutex.Lock()
		ai.mockHandlePeerAnnounce(peer2Addr, "eth0")
		ai.Mutex.Unlock()

		// Give a small amount of time for the peer to be processed
		time.Sleep(10 * time.Millisecond)

		ai.Mutex.RLock()
		count := len(ai.peers)
		_, exists := ai.peers[peer2AddrStr]
		ai.Mutex.RUnlock()

		if count != 2 {
			t.Fatalf("Expected 2 peers, got %d", count)
		}
		if !exists {
			t.Fatalf("Peer %s not found in map", peer2AddrStr)
		}
	})

	t.Run("IgnoreLocalAnnounce", func(t *testing.T) {
		ai.Mutex.Lock()
		ai.mockHandlePeerAnnounce(localAddr, "eth0")
		ai.Mutex.Unlock()

		// Give a small amount of time for the peer to be processed
		time.Sleep(10 * time.Millisecond)

		ai.Mutex.RLock()
		count := len(ai.peers)
		ai.Mutex.RUnlock()

		if count != 2 {
			t.Fatalf("Expected 2 peers after local announce, got %d", count)
		}
	})

	t.Run("UpdatePeerTimestamp", func(t *testing.T) {
		ai.Mutex.RLock()
		peer, exists := ai.peers[peer1AddrStr]
		var initialTime time.Time
		if exists {
			initialTime = peer.lastHeard
		}
		ai.Mutex.RUnlock()

		if !exists {
			t.Fatalf("Peer %s not found before timestamp update", peer1AddrStr)
		}

		ai.Mutex.Lock()
		ai.mockHandlePeerAnnounce(peer1Addr, "eth0")
		ai.Mutex.Unlock()

		// Give a small amount of time for the peer to be processed
		time.Sleep(10 * time.Millisecond)

		ai.Mutex.RLock()
		peer, exists = ai.peers[peer1AddrStr]
		var updatedTime time.Time
		if exists {
			updatedTime = peer.lastHeard
		}
		ai.Mutex.RUnlock()

		if !exists {
			t.Fatalf("Peer %s not found after timestamp update", peer1AddrStr)
		}

		if !updatedTime.After(initialTime) {
			t.Errorf("Peer timestamp was not updated after receiving another announce")
		}
	})

	t.Run("PeerTimeout", func(t *testing.T) {
		// Wait for peer timeout
		time.Sleep(testTimeout * 2)

		ai.Mutex.RLock()
		count := len(ai.peers)
		ai.Mutex.RUnlock()

		if count != 0 {
			t.Errorf("Expected all peers to timeout, got %d peers", count)
		}
	})
}
