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

package netmon

import (
	"flag"
	"net"
	"net/netip"
	"sync/atomic"
	"testing"
	"time"

	"tailscale.com/net/interfaces"
	"tailscale.com/util/mak"
)

func TestMonitorStartClose(t *testing.T) {
	mon, err := New(t.Logf)
	if err != nil {
		t.Fatal(err)
	}
	mon.Start()
	if err := mon.Close(); err != nil {
		t.Fatal(err)
	}
}

func TestMonitorJustClose(t *testing.T) {
	mon, err := New(t.Logf)
	if err != nil {
		t.Fatal(err)
	}
	if err := mon.Close(); err != nil {
		t.Fatal(err)
	}
}

func TestMonitorInjectEvent(t *testing.T) {
	mon, err := New(t.Logf)
	if err != nil {
		t.Fatal(err)
	}
	defer mon.Close()
	got := make(chan bool, 1)
	mon.RegisterChangeCallback(func(*ChangeDelta) {
		select {
		case got <- true:
		default:
		}
	})
	mon.Start()
	mon.InjectEvent()
	select {
	case <-got:
		// Pass.
	case <-time.After(5 * time.Second):
		t.Fatal("timeout waiting for callback")
	}
}

var (
	monitor         = flag.String("monitor", "", `go into monitor mode like 'route monitor'; test never terminates. Value can be either "raw" or "callback"`)
	monitorDuration = flag.Duration("monitor-duration", 0, "if non-zero, how long to run TestMonitorMode. Zero means forever.")
)

func TestMonitorMode(t *testing.T) {
	switch *monitor {
	case "":
		t.Skip("skipping non-test without --monitor")
	case "raw", "callback":
	default:
		t.Skipf(`invalid --monitor value: must be "raw" or "callback"`)
	}
	mon, err := New(t.Logf)
	if err != nil {
		t.Fatal(err)
	}
	switch *monitor {
	case "raw":
		var closed atomic.Bool
		if *monitorDuration != 0 {
			t := time.AfterFunc(*monitorDuration, func() {
				closed.Store(true)
				mon.Close()
			})
			defer t.Stop()
		}
		for {
			msg, err := mon.om.Receive()
			if closed.Load() {
				return
			}
			if err != nil {
				t.Fatal(err)
			}
			t.Logf("msg: %#v", msg)
		}
	case "callback":
		var done <-chan time.Time
		if *monitorDuration != 0 {
			t := time.NewTimer(*monitorDuration)
			defer t.Stop()
			done = t.C
		}
		n := 0
		mon.RegisterChangeCallback(func(d *ChangeDelta) {
			n++
			t.Logf("cb: changed=%v, ifSt=%v", d.Major, d.New)
		})
		mon.Start()
		<-done
		t.Logf("%v callbacks", n)
	}
}

// tests (*State).IsMajorChangeFrom
func TestIsMajorChangeFrom(t *testing.T) {
	type State = interfaces.State
	type Interface = interfaces.Interface
	tests := []struct {
		name   string
		s1, s2 *State
		want   bool
	}{
		{
			name: "eq_nil",
			want: false,
		},
		{
			name: "nil_mix",
			s2:   new(State),
			want: true,
		},
		{
			name: "eq",
			s1: &State{
				DefaultRouteInterface: "foo",
				InterfaceIPs: map[string][]netip.Prefix{
					"foo": {netip.MustParsePrefix("10.0.1.2/16")},
				},
			},
			s2: &State{
				DefaultRouteInterface: "foo",
				InterfaceIPs: map[string][]netip.Prefix{
					"foo": {netip.MustParsePrefix("10.0.1.2/16")},
				},
			},
			want: false,
		},
		{
			name: "default-route-changed",
			s1: &State{
				DefaultRouteInterface: "foo",
				InterfaceIPs: map[string][]netip.Prefix{
					"foo": {netip.MustParsePrefix("10.0.1.2/16")},
				},
			},
			s2: &State{
				DefaultRouteInterface: "bar",
				InterfaceIPs: map[string][]netip.Prefix{
					"foo": {netip.MustParsePrefix("10.0.1.2/16")},
				},
			},
			want: true,
		},
		{
			name: "some-interesting-ip-changed",
			s1: &State{
				DefaultRouteInterface: "foo",
				InterfaceIPs: map[string][]netip.Prefix{
					"foo": {netip.MustParsePrefix("10.0.1.2/16")},
				},
			},
			s2: &State{
				DefaultRouteInterface: "foo",
				InterfaceIPs: map[string][]netip.Prefix{
					"foo": {netip.MustParsePrefix("10.0.1.3/16")},
				},
			},
			want: true,
		},
		{
			name: "ipv6-ula-addressed-appeared",
			s1: &State{
				DefaultRouteInterface: "foo",
				InterfaceIPs: map[string][]netip.Prefix{
					"foo": {netip.MustParsePrefix("10.0.1.2/16")},
				},
			},
			s2: &State{
				DefaultRouteInterface: "foo",
				InterfaceIPs: map[string][]netip.Prefix{
					"foo": {
						netip.MustParsePrefix("10.0.1.2/16"),
						// Brad saw this address coming & going on his home LAN, possibly
						// via an Apple TV Thread routing advertisement? (Issue 9040)
						netip.MustParsePrefix("fd15:bbfa:c583:4fce:f4fb:4ff:fe1a:4148/64"),
					},
				},
			},
			want: true, // TODO(bradfitz): want false (ignore the IPv6 ULA address on foo)
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Populate dummy interfaces where missing.
			for _, s := range []*State{tt.s1, tt.s2} {
				if s == nil {
					continue
				}
				for name := range s.InterfaceIPs {
					if _, ok := s.Interface[name]; !ok {
						mak.Set(&s.Interface, name, Interface{Interface: &net.Interface{
							Name: name,
						}})
					}
				}
			}

			var m Monitor
			m.om = &testOSMon{
				Interesting: func(name string) bool { return true },
			}
			if got := m.IsMajorChangeFrom(tt.s1, tt.s2); got != tt.want {
				t.Errorf("IsMajorChange = %v; want %v", got, tt.want)
			}
		})
	}
}

type testOSMon struct {
	osMon
	Interesting func(name string) bool
}

func (m *testOSMon) IsInterestingInterface(name string) bool {
	if m.Interesting == nil {
		return true
	}
	return m.Interesting(name)
}