// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

package portlist

import (
	"context"
	"flag"
	"net"
	"runtime"
	"sync"
	"testing"
	"time"

	"tailscale.com/tstest"
)

func TestGetList(t *testing.T) {
	tstest.ResourceCheck(t)

	var p Poller
	pl, err := p.getList()
	if err != nil {
		t.Fatal(err)
	}
	for i, p := range pl {
		t.Logf("[%d] %+v", i, p)
	}
	t.Logf("As String: %v", pl.String())
}

func TestIgnoreLocallyBoundPorts(t *testing.T) {
	tstest.ResourceCheck(t)

	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Skipf("failed to bind: %v", err)
	}
	defer ln.Close()
	ta := ln.Addr().(*net.TCPAddr)
	port := ta.Port
	var p Poller
	pl, err := p.getList()
	if err != nil {
		t.Fatal(err)
	}
	for _, p := range pl {
		if p.Proto == "tcp" && int(p.Port) == port {
			t.Fatal("didn't expect to find test's localhost ephemeral port")
		}
	}
}

var flagRunUnspecTests = flag.Bool("run-unspec-tests",
	runtime.GOOS == "linux", // other OSes have annoying firewall GUI confirmation dialogs
	"run tests that require listening on the the unspecified address")

func TestChangesOverTime(t *testing.T) {
	if !*flagRunUnspecTests {
		t.Skip("skipping test without --run-unspec-tests")
	}

	var p Poller
	get := func(t *testing.T) []Port {
		t.Helper()
		s, err := p.getList()
		if err != nil {
			t.Fatal(err)
		}
		return append([]Port(nil), s...)
	}

	p1 := get(t)
	ln, err := net.Listen("tcp", ":0")
	if err != nil {
		t.Skipf("failed to bind: %v", err)
	}
	defer ln.Close()
	port := uint16(ln.Addr().(*net.TCPAddr).Port)
	containsPort := func(pl List) bool {
		for _, p := range pl {
			if p.Proto == "tcp" && p.Port == port {
				return true
			}
		}
		return false
	}
	if containsPort(p1) {
		t.Error("unexpectedly found ephemeral port in p1, before it was opened", port)
	}
	p2 := get(t)
	if !containsPort(p2) {
		t.Error("didn't find ephemeral port in p2", port)
	}
	ln.Close()
	p3 := get(t)
	if containsPort(p3) {
		t.Error("unexpectedly found ephemeral port in p3, after it was closed", port)
	}
}

func TestEqualLessThan(t *testing.T) {
	tests := []struct {
		name string
		a, b Port
		want bool
	}{
		{
			"Port a < b",
			Port{Proto: "tcp", Port: 100, Process: "proc1"},
			Port{Proto: "tcp", Port: 101, Process: "proc1"},
			true,
		},
		{
			"Port a > b",
			Port{Proto: "tcp", Port: 101, Process: "proc1"},
			Port{Proto: "tcp", Port: 100, Process: "proc1"},
			false,
		},
		{
			"Proto a < b",
			Port{Proto: "tcp", Port: 100, Process: "proc1"},
			Port{Proto: "udp", Port: 100, Process: "proc1"},
			true,
		},
		{
			"Proto a < b",
			Port{Proto: "udp", Port: 100, Process: "proc1"},
			Port{Proto: "tcp", Port: 100, Process: "proc1"},
			false,
		},
		{
			"Process a < b",
			Port{Proto: "tcp", Port: 100, Process: "proc1"},
			Port{Proto: "tcp", Port: 100, Process: "proc2"},
			true,
		},
		{
			"Process a > b",
			Port{Proto: "tcp", Port: 100, Process: "proc2"},
			Port{Proto: "tcp", Port: 100, Process: "proc1"},
			false,
		},
		{
			"Port evaluated first",
			Port{Proto: "udp", Port: 100, Process: "proc2"},
			Port{Proto: "tcp", Port: 101, Process: "proc1"},
			true,
		},
		{
			"Proto evaluated second",
			Port{Proto: "tcp", Port: 100, Process: "proc2"},
			Port{Proto: "udp", Port: 100, Process: "proc1"},
			true,
		},
		{
			"Process evaluated fourth",
			Port{Proto: "tcp", Port: 100, Process: "proc1"},
			Port{Proto: "tcp", Port: 100, Process: "proc2"},
			true,
		},
		{
			"equal",
			Port{Proto: "tcp", Port: 100, Process: "proc1"},
			Port{Proto: "tcp", Port: 100, Process: "proc1"},
			false,
		},
	}

	for _, tt := range tests {
		got := tt.a.lessThan(&tt.b)
		if got != tt.want {
			t.Errorf("%s: Equal = %v; want %v", tt.name, got, tt.want)
		}
		lessBack := tt.b.lessThan(&tt.a)
		if got && lessBack {
			t.Errorf("%s: both a and b report being less than each other", tt.name)
		}
		wantEqual := !got && !lessBack
		gotEqual := tt.a.equal(&tt.b)
		if gotEqual != wantEqual {
			t.Errorf("%s: equal = %v; want %v", tt.name, gotEqual, wantEqual)
		}
	}
}

func TestPoller(t *testing.T) {
	p, err := NewPoller()
	if err != nil {
		t.Skipf("not running test: %v", err)
	}
	defer p.Close()

	var wg sync.WaitGroup
	wg.Add(2)

	gotUpdate := make(chan bool, 16)

	go func() {
		defer wg.Done()
		for pl := range p.Updates() {
			// Look at all the pl slice memory to maximize
			// chance of race detector seeing violations.
			for _, v := range pl {
				if v == (Port{}) {
					// Force use
					panic("empty port")
				}
			}
			select {
			case gotUpdate <- true:
			default:
			}
		}
	}()

	tick := make(chan time.Time, 16)
	go func() {
		defer wg.Done()
		if err := p.runWithTickChan(context.Background(), tick); err != nil {
			t.Error("runWithTickChan:", err)
		}
	}()
	for i := 0; i < 10; i++ {
		ln, err := net.Listen("tcp", ":0")
		if err != nil {
			t.Fatal(err)
		}
		defer ln.Close()
		tick <- time.Time{}

		select {
		case <-gotUpdate:
		case <-time.After(5 * time.Second):
			t.Fatal("timed out waiting for update")
		}
	}

	// And a bunch of ticks without waiting for updates,
	// to make race tests more likely to fail, if any present.
	for i := 0; i < 10; i++ {
		tick <- time.Time{}
	}

	if err := p.Close(); err != nil {
		t.Fatal(err)
	}
	wg.Wait()
}

func BenchmarkGetList(b *testing.B) {
	benchmarkGetList(b, false)
}

func BenchmarkGetListIncremental(b *testing.B) {
	benchmarkGetList(b, true)
}

func benchmarkGetList(b *testing.B, incremental bool) {
	b.ReportAllocs()
	var p Poller
	for i := 0; i < b.N; i++ {
		pl, err := p.getList()
		if err != nil {
			b.Fatal(err)
		}
		if incremental {
			p.prev = pl
		}
	}
}