package portallocator

import (
	"errors"
	"net"
	"testing"

	"gotest.tools/v3/assert"
	is "gotest.tools/v3/assert/cmp"
)

func TestRequestNewPort(t *testing.T) {
	p := newInstance()

	port, err := p.RequestPort(net.IPv4zero, "tcp", 0)
	if err != nil {
		t.Fatal(err)
	}

	if expected := p.begin; port != expected {
		t.Fatalf("Expected port %d got %d", expected, port)
	}
}

func TestRequestSpecificPort(t *testing.T) {
	p := newInstance()

	port, err := p.RequestPort(net.IPv4zero, "tcp", 5000)
	if err != nil {
		t.Fatal(err)
	}

	if port != 5000 {
		t.Fatalf("Expected port 5000 got %d", port)
	}
}

func TestReleasePort(t *testing.T) {
	p := newInstance()

	port, err := p.RequestPort(net.IPv4zero, "tcp", 5000)
	if err != nil {
		t.Fatal(err)
	}
	if port != 5000 {
		t.Fatalf("Expected port 5000 got %d", port)
	}

	p.ReleasePort(net.IPv4zero, "tcp", 5000)
}

func TestReuseReleasedPort(t *testing.T) {
	p := newInstance()

	port, err := p.RequestPort(net.IPv4zero, "tcp", 5000)
	if err != nil {
		t.Fatal(err)
	}
	if port != 5000 {
		t.Fatalf("Expected port 5000 got %d", port)
	}

	p.ReleasePort(net.IPv4zero, "tcp", 5000)

	port, err = p.RequestPort(net.IPv4zero, "tcp", 5000)
	if err != nil {
		t.Fatal(err)
	}
	if port != 5000 {
		t.Fatalf("Expected port 5000 got %d", port)
	}
}

func TestReleaseUnreadledPort(t *testing.T) {
	p := newInstance()

	port, err := p.RequestPort(net.IPv4zero, "tcp", 5000)
	if err != nil {
		t.Fatal(err)
	}
	if port != 5000 {
		t.Fatalf("Expected port 5000 got %d", port)
	}

	_, err = p.RequestPort(net.IPv4zero, "tcp", 5000)

	var expectedErrType alreadyAllocatedErr
	if !errors.As(err, &expectedErrType) {
		t.Fatalf("Expected port allocation error got %s", err)
	}
}

func TestUnknowProtocol(t *testing.T) {
	p := newInstance()

	if _, err := p.RequestPort(net.IPv4zero, "tcpp", 0); !errors.Is(err, errUnknownProtocol) {
		t.Fatalf("Expected error %s got %s", errUnknownProtocol, err)
	}
}

func TestAllocateAllPorts(t *testing.T) {
	p := newInstance()

	for i := 0; i <= p.end-p.begin; i++ {
		port, err := p.RequestPort(net.IPv4zero, "tcp", 0)
		if err != nil {
			t.Fatal(err)
		}

		if expected := p.begin + i; port != expected {
			t.Fatalf("Expected port %d got %d", expected, port)
		}
	}

	if _, err := p.RequestPort(net.IPv4zero, "tcp", 0); !errors.Is(err, errAllPortsAllocated) {
		t.Fatalf("Expected error %s got %s", errAllPortsAllocated, err)
	}

	_, err := p.RequestPort(net.IPv4zero, "udp", 0)
	if err != nil {
		t.Fatal(err)
	}

	// release a port in the middle and ensure we get another tcp port
	port := p.begin + 5
	p.ReleasePort(net.IPv4zero, "tcp", port)
	newPort, err := p.RequestPort(net.IPv4zero, "tcp", 0)
	if err != nil {
		t.Fatal(err)
	}
	if newPort != port {
		t.Fatalf("Expected port %d got %d", port, newPort)
	}

	// now pm.last == newPort, release it so that it's the only free port of
	// the range, and ensure we get it back
	p.ReleasePort(net.IPv4zero, "tcp", newPort)
	port, err = p.RequestPort(net.IPv4zero, "tcp", 0)
	if err != nil {
		t.Fatal(err)
	}
	if newPort != port {
		t.Fatalf("Expected port %d got %d", newPort, port)
	}
}

func BenchmarkAllocatePorts(b *testing.B) {
	p := newInstance()

	for n := 0; n < b.N; n++ {
		for i := 0; i <= p.end-p.begin; i++ {
			port, err := p.RequestPort(net.IPv4zero, "tcp", 0)
			if err != nil {
				b.Fatal(err)
			}

			if expected := p.begin + i; port != expected {
				b.Fatalf("Expected port %d got %d", expected, port)
			}
		}
		p.ReleaseAll()
	}
}

func TestPortAllocation(t *testing.T) {
	p := newInstance()

	ip := net.ParseIP("192.168.0.1")
	ip2 := net.ParseIP("192.168.0.2")
	if port, err := p.RequestPort(ip, "tcp", 80); err != nil {
		t.Fatal(err)
	} else if port != 80 {
		t.Fatalf("Acquire(80) should return 80, not %d", port)
	}
	port, err := p.RequestPort(ip, "tcp", 0)
	if err != nil {
		t.Fatal(err)
	}
	if port <= 0 {
		t.Fatalf("Acquire(0) should return a non-zero port")
	}

	if _, err := p.RequestPort(ip, "tcp", port); err == nil {
		t.Fatalf("Acquiring a port already in use should return an error")
	}

	if newPort, err := p.RequestPort(ip, "tcp", 0); err != nil {
		t.Fatal(err)
	} else if newPort == port {
		t.Fatalf("Acquire(0) allocated the same port twice: %d", port)
	}

	if _, err := p.RequestPort(ip, "tcp", 80); err == nil {
		t.Fatalf("Acquiring a port already in use should return an error")
	}
	if _, err := p.RequestPort(ip2, "tcp", 80); err != nil {
		t.Fatalf("It should be possible to allocate the same port on a different interface")
	}
	if _, err := p.RequestPort(ip2, "tcp", 80); err == nil {
		t.Fatalf("Acquiring a port already in use should return an error")
	}
	p.ReleasePort(ip, "tcp", 80)
	if _, err := p.RequestPort(ip, "tcp", 80); err != nil {
		t.Fatal(err)
	}

	port, err = p.RequestPort(ip, "tcp", 0)
	if err != nil {
		t.Fatal(err)
	}
	port2, err := p.RequestPort(ip, "tcp", port+1)
	if err != nil {
		t.Fatal(err)
	}
	port3, err := p.RequestPort(ip, "tcp", 0)
	if err != nil {
		t.Fatal(err)
	}
	if port3 == port2 {
		t.Fatal("Requesting a dynamic port should never allocate a used port")
	}
}

func TestPortAllocationWithCustomRange(t *testing.T) {
	p := newInstance()

	start, end := 8081, 8082
	specificPort := 8000

	// get an ephemeral port.
	port1, err := p.RequestPortInRange(net.IPv4zero, "tcp", 0, 0)
	if err != nil {
		t.Fatal(err)
	}

	// request invalid ranges
	if _, err := p.RequestPortInRange(net.IPv4zero, "tcp", 0, end); err == nil {
		t.Fatalf("Expected error for invalid range %d-%d", 0, end)
	}
	if _, err := p.RequestPortInRange(net.IPv4zero, "tcp", start, 0); err == nil {
		t.Fatalf("Expected error for invalid range %d-%d", 0, end)
	}
	if _, err := p.RequestPortInRange(net.IPv4zero, "tcp", 8081, 8080); err == nil {
		t.Fatalf("Expected error for invalid range %d-%d", 0, end)
	}

	// request a single port
	port, err := p.RequestPortInRange(net.IPv4zero, "tcp", specificPort, specificPort)
	if err != nil {
		t.Fatal(err)
	}
	if port != specificPort {
		t.Fatalf("Expected port %d, got %d", specificPort, port)
	}

	// get a port from the range
	port2, err := p.RequestPortInRange(net.IPv4zero, "tcp", start, end)
	if err != nil {
		t.Fatal(err)
	}
	if port2 < start || port2 > end {
		t.Fatalf("Expected a port between %d and %d, got %d", start, end, port2)
	}
	// get another ephemeral port (should be > port1)
	port3, err := p.RequestPortInRange(net.IPv4zero, "tcp", 0, 0)
	if err != nil {
		t.Fatal(err)
	}
	if port3 < port1 {
		t.Fatalf("Expected new port > %d in the ephemeral range, got %d", port1, port3)
	}
	// get another (and in this case the only other) port from the range
	port4, err := p.RequestPortInRange(net.IPv4zero, "tcp", start, end)
	if err != nil {
		t.Fatal(err)
	}
	if port4 < start || port4 > end {
		t.Fatalf("Expected a port between %d and %d, got %d", start, end, port4)
	}
	if port4 == port2 {
		t.Fatal("Allocated the same port from a custom range")
	}
	// request 3rd port from the range of 2
	if _, err := p.RequestPortInRange(net.IPv4zero, "tcp", start, end); !errors.Is(err, errAllPortsAllocated) {
		t.Fatalf("Expected error %s got %s", errAllPortsAllocated, err)
	}
}

func TestNoDuplicateBPR(t *testing.T) {
	p := newInstance()

	if port, err := p.RequestPort(net.IPv4zero, "tcp", p.begin); err != nil {
		t.Fatal(err)
	} else if port != p.begin {
		t.Fatalf("Expected port %d got %d", p.begin, port)
	}

	if port, err := p.RequestPort(net.IPv4zero, "tcp", 0); err != nil {
		t.Fatal(err)
	} else if port == p.begin {
		t.Fatalf("Acquire(0) allocated the same port twice: %d", port)
	}
}

func TestRequestPortForMultipleIPs(t *testing.T) {
	p := newInstance()

	addrs := []net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::")}

	// Default port range.
	port, err := p.RequestPortsInRange(addrs, "tcp", 0, 0)
	assert.Check(t, err)
	assert.Check(t, is.Equal(port, p.begin))

	// Single-port range.
	port, err = p.RequestPortsInRange(addrs, "tcp", 10000, 10000)
	assert.Check(t, err)
	assert.Check(t, is.Equal(port, 10000))

	// Same single-port range, expect an error.
	_, err = p.RequestPortsInRange(addrs, "tcp", 10000, 10000)
	assert.Check(t, is.ErrorContains(err, "port is already allocated"))

	// Release the port from one address.
	p.ReleasePort(addrs[0], "tcp", 10000)

	// Same single-port range, still expect an error because the port's still held
	// for the second address.
	_, err = p.RequestPortsInRange(addrs, "tcp", 10000, 10000)
	assert.Check(t, is.Error(err, "Bind for :::10000 failed: port is already allocated"))

	// Release the port from the other address.
	p.ReleasePort(addrs[1], "tcp", 10000)

	// Should now be able to re-allocate the port.
	port, err = p.RequestPortsInRange(addrs, "tcp", 10000, 10000)
	assert.Check(t, err)
	assert.Check(t, is.Equal(port, 10000))

	// Multi-port range.
	for i := 20000; i < 20004; i += 1 {
		port, err = p.RequestPortsInRange(addrs, "tcp", 20000, 20004)
		assert.Check(t, err)
		assert.Check(t, is.Equal(port, i))
	}
}

func TestMixUnspecAndSpecificAddrs(t *testing.T) {
	p := newInstance()

	port, err := p.RequestPort(net.IPv4(127, 0, 0, 1), "udp", 0)
	assert.Check(t, err)
	assert.Check(t, is.Equal(port, p.begin))

	port, err = p.RequestPort(net.IPv4zero, "udp", 0)
	assert.Check(t, err)
	assert.Check(t, is.Equal(port, p.begin+1))
}
