package integration

import (
	"fmt"
	"log"
	"net/netip"
	"sort"
	"strconv"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
	"github.com/juanfont/headscale/hscontrol/policy"
	"github.com/juanfont/headscale/hscontrol/util"
	"github.com/juanfont/headscale/integration/hsic"
	"github.com/juanfont/headscale/integration/tsic"
	"github.com/stretchr/testify/assert"
	"tailscale.com/types/ipproto"
	"tailscale.com/types/views"
	"tailscale.com/wgengine/filter"
)

// This test is both testing the routes command and the propagation of
// routes.
func TestEnablingRoutes(t *testing.T) {
	IntegrationSkip(t)
	t.Parallel()

	user := "enable-routing"

	scenario, err := NewScenario(dockertestMaxWait())
	assertNoErrf(t, "failed to create scenario: %s", err)
	defer scenario.Shutdown()

	spec := map[string]int{
		user: 3,
	}

	err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clienableroute"))
	assertNoErrHeadscaleEnv(t, err)

	allClients, err := scenario.ListTailscaleClients()
	assertNoErrListClients(t, err)

	err = scenario.WaitForTailscaleSync()
	assertNoErrSync(t, err)

	headscale, err := scenario.Headscale()
	assertNoErrGetHeadscale(t, err)

	expectedRoutes := map[string]string{
		"1": "10.0.0.0/24",
		"2": "10.0.1.0/24",
		"3": "10.0.2.0/24",
	}

	// advertise routes using the up command
	for _, client := range allClients {
		status, err := client.Status()
		assertNoErr(t, err)

		command := []string{
			"tailscale",
			"set",
			"--advertise-routes=" + expectedRoutes[string(status.Self.ID)],
		}
		_, _, err = client.Execute(command)
		assertNoErrf(t, "failed to advertise route: %s", err)
	}

	err = scenario.WaitForTailscaleSync()
	assertNoErrSync(t, err)

	var routes []*v1.Route
	err = executeAndUnmarshal(
		headscale,
		[]string{
			"headscale",
			"routes",
			"list",
			"--output",
			"json",
		},
		&routes,
	)

	assertNoErr(t, err)
	assert.Len(t, routes, 3)

	for _, route := range routes {
		assert.Equal(t, true, route.GetAdvertised())
		assert.Equal(t, false, route.GetEnabled())
		assert.Equal(t, false, route.GetIsPrimary())
	}

	// Verify that no routes has been sent to the client,
	// they are not yet enabled.
	for _, client := range allClients {
		status, err := client.Status()
		assertNoErr(t, err)

		for _, peerKey := range status.Peers() {
			peerStatus := status.Peer[peerKey]

			assert.Nil(t, peerStatus.PrimaryRoutes)
		}
	}

	// Enable all routes
	for _, route := range routes {
		_, err = headscale.Execute(
			[]string{
				"headscale",
				"routes",
				"enable",
				"--route",
				strconv.Itoa(int(route.GetId())),
			})
		assertNoErr(t, err)
	}

	var enablingRoutes []*v1.Route
	err = executeAndUnmarshal(
		headscale,
		[]string{
			"headscale",
			"routes",
			"list",
			"--output",
			"json",
		},
		&enablingRoutes,
	)
	assertNoErr(t, err)
	assert.Len(t, enablingRoutes, 3)

	for _, route := range enablingRoutes {
		assert.Equal(t, true, route.GetAdvertised())
		assert.Equal(t, true, route.GetEnabled())
		assert.Equal(t, true, route.GetIsPrimary())
	}

	time.Sleep(5 * time.Second)

	// Verify that the clients can see the new routes
	for _, client := range allClients {
		status, err := client.Status()
		assertNoErr(t, err)

		for _, peerKey := range status.Peers() {
			peerStatus := status.Peer[peerKey]

			assert.NotNil(t, peerStatus.PrimaryRoutes)
			if peerStatus.PrimaryRoutes == nil {
				continue
			}

			pRoutes := peerStatus.PrimaryRoutes.AsSlice()

			assert.Len(t, pRoutes, 1)

			if len(pRoutes) > 0 {
				peerRoute := peerStatus.PrimaryRoutes.AsSlice()[0]

				// id starts at 1, we created routes with 0 index
				assert.Equalf(
					t,
					expectedRoutes[string(peerStatus.ID)],
					peerRoute.String(),
					"expected route %s to be present on peer %s (%s) in %s (%s) status",
					expectedRoutes[string(peerStatus.ID)],
					peerStatus.HostName,
					peerStatus.ID,
					client.Hostname(),
					client.ID(),
				)
			}
		}
	}

	routeToBeDisabled := enablingRoutes[0]
	log.Printf("preparing to disable %v", routeToBeDisabled)

	_, err = headscale.Execute(
		[]string{
			"headscale",
			"routes",
			"disable",
			"--route",
			strconv.Itoa(int(routeToBeDisabled.GetId())),
		})
	assertNoErr(t, err)

	time.Sleep(5 * time.Second)

	var disablingRoutes []*v1.Route
	err = executeAndUnmarshal(
		headscale,
		[]string{
			"headscale",
			"routes",
			"list",
			"--output",
			"json",
		},
		&disablingRoutes,
	)
	assertNoErr(t, err)

	for _, route := range disablingRoutes {
		assert.Equal(t, true, route.GetAdvertised())

		if route.GetId() == routeToBeDisabled.GetId() {
			assert.Equal(t, false, route.GetEnabled())

			// since this is the only route of this cidr,
			// it will not failover, and remain Primary
			// until something can replace it.
			assert.Equal(t, true, route.GetIsPrimary())
		} else {
			assert.Equal(t, true, route.GetEnabled())
			assert.Equal(t, true, route.GetIsPrimary())
		}
	}

	// Verify that the clients can see the new routes
	for _, client := range allClients {
		status, err := client.Status()
		assertNoErr(t, err)

		for _, peerKey := range status.Peers() {
			peerStatus := status.Peer[peerKey]

			if string(peerStatus.ID) == fmt.Sprintf("%d", routeToBeDisabled.GetNode().GetId()) {
				assert.Nilf(
					t,
					peerStatus.PrimaryRoutes,
					"expected node %s to have no routes, got primary route (%v)",
					peerStatus.HostName,
					peerStatus.PrimaryRoutes,
				)
			}
		}
	}
}

func TestHASubnetRouterFailover(t *testing.T) {
	IntegrationSkip(t)
	t.Parallel()

	user := "enable-routing"

	scenario, err := NewScenario(dockertestMaxWait())
	assertNoErrf(t, "failed to create scenario: %s", err)
	defer scenario.Shutdown()

	spec := map[string]int{
		user: 3,
	}

	err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clienableroute"))
	assertNoErrHeadscaleEnv(t, err)

	allClients, err := scenario.ListTailscaleClients()
	assertNoErrListClients(t, err)

	err = scenario.WaitForTailscaleSync()
	assertNoErrSync(t, err)

	headscale, err := scenario.Headscale()
	assertNoErrGetHeadscale(t, err)

	expectedRoutes := map[string]string{
		"1": "10.0.0.0/24",
		"2": "10.0.0.0/24",
	}

	// Sort nodes by ID
	sort.SliceStable(allClients, func(i, j int) bool {
		statusI, err := allClients[i].Status()
		if err != nil {
			return false
		}

		statusJ, err := allClients[j].Status()
		if err != nil {
			return false
		}

		return statusI.Self.ID < statusJ.Self.ID
	})

	subRouter1 := allClients[0]
	subRouter2 := allClients[1]

	client := allClients[2]

	t.Logf("Advertise route from r1 (%s) and r2 (%s), making it HA, n1 is primary", subRouter1.Hostname(), subRouter2.Hostname())
	// advertise HA route on node 1 and 2
	// ID 1 will be primary
	// ID 2 will be secondary
	for _, client := range allClients[:2] {
		status, err := client.Status()
		assertNoErr(t, err)

		if route, ok := expectedRoutes[string(status.Self.ID)]; ok {
			command := []string{
				"tailscale",
				"set",
				"--advertise-routes=" + route,
			}
			_, _, err = client.Execute(command)
			assertNoErrf(t, "failed to advertise route: %s", err)
		} else {
			t.Fatalf("failed to find route for Node %s (id: %s)", status.Self.HostName, status.Self.ID)
		}
	}

	err = scenario.WaitForTailscaleSync()
	assertNoErrSync(t, err)

	var routes []*v1.Route
	err = executeAndUnmarshal(
		headscale,
		[]string{
			"headscale",
			"routes",
			"list",
			"--output",
			"json",
		},
		&routes,
	)

	assertNoErr(t, err)
	assert.Len(t, routes, 2)

	t.Logf("initial routes %#v", routes)

	for _, route := range routes {
		assert.Equal(t, true, route.GetAdvertised())
		assert.Equal(t, false, route.GetEnabled())
		assert.Equal(t, false, route.GetIsPrimary())
	}

	// Verify that no routes has been sent to the client,
	// they are not yet enabled.
	for _, client := range allClients {
		status, err := client.Status()
		assertNoErr(t, err)

		for _, peerKey := range status.Peers() {
			peerStatus := status.Peer[peerKey]

			assert.Nil(t, peerStatus.PrimaryRoutes)
		}
	}

	// Enable all routes
	for _, route := range routes {
		_, err = headscale.Execute(
			[]string{
				"headscale",
				"routes",
				"enable",
				"--route",
				strconv.Itoa(int(route.GetId())),
			})
		assertNoErr(t, err)

		time.Sleep(time.Second)
	}

	var enablingRoutes []*v1.Route
	err = executeAndUnmarshal(
		headscale,
		[]string{
			"headscale",
			"routes",
			"list",
			"--output",
			"json",
		},
		&enablingRoutes,
	)
	assertNoErr(t, err)
	assert.Len(t, enablingRoutes, 2)

	// Node 1 is primary
	assert.Equal(t, true, enablingRoutes[0].GetAdvertised())
	assert.Equal(t, true, enablingRoutes[0].GetEnabled())
	assert.Equal(t, true, enablingRoutes[0].GetIsPrimary(), "both subnet routers are up, expected r1 to be primary")

	// Node 2 is not primary
	assert.Equal(t, true, enablingRoutes[1].GetAdvertised())
	assert.Equal(t, true, enablingRoutes[1].GetEnabled())
	assert.Equal(t, false, enablingRoutes[1].GetIsPrimary(), "both subnet routers are up, expected r2 to be non-primary")

	// Verify that the client has routes from the primary machine
	srs1, err := subRouter1.Status()
	srs2, err := subRouter2.Status()

	clientStatus, err := client.Status()
	assertNoErr(t, err)

	srs1PeerStatus := clientStatus.Peer[srs1.Self.PublicKey]
	srs2PeerStatus := clientStatus.Peer[srs2.Self.PublicKey]

	assert.True(t, srs1PeerStatus.Online, "r1 up, r2 up")
	assert.True(t, srs2PeerStatus.Online, "r1 up, r2 up")

	assertNotNil(t, srs1PeerStatus.PrimaryRoutes)
	assert.Nil(t, srs2PeerStatus.PrimaryRoutes)

	assert.Contains(
		t,
		srs1PeerStatus.PrimaryRoutes.AsSlice(),
		netip.MustParsePrefix(expectedRoutes[string(srs1.Self.ID)]),
	)

	// Take down the current primary
	t.Logf("taking down subnet router r1 (%s)", subRouter1.Hostname())
	t.Logf("expecting r2 (%s) to take over as primary", subRouter2.Hostname())
	err = subRouter1.Down()
	assertNoErr(t, err)

	time.Sleep(5 * time.Second)

	var routesAfterMove []*v1.Route
	err = executeAndUnmarshal(
		headscale,
		[]string{
			"headscale",
			"routes",
			"list",
			"--output",
			"json",
		},
		&routesAfterMove,
	)
	assertNoErr(t, err)
	assert.Len(t, routesAfterMove, 2)

	// Node 1 is not primary
	assert.Equal(t, true, routesAfterMove[0].GetAdvertised())
	assert.Equal(t, true, routesAfterMove[0].GetEnabled())
	assert.Equal(t, false, routesAfterMove[0].GetIsPrimary(), "r1 is down, expected r2 to be primary")

	// Node 2 is primary
	assert.Equal(t, true, routesAfterMove[1].GetAdvertised())
	assert.Equal(t, true, routesAfterMove[1].GetEnabled())
	assert.Equal(t, true, routesAfterMove[1].GetIsPrimary(), "r1 is down, expected r2 to be primary")

	srs2, err = subRouter2.Status()

	clientStatus, err = client.Status()
	assertNoErr(t, err)

	srs1PeerStatus = clientStatus.Peer[srs1.Self.PublicKey]
	srs2PeerStatus = clientStatus.Peer[srs2.Self.PublicKey]

	assert.False(t, srs1PeerStatus.Online, "r1 down, r2 down")
	assert.True(t, srs2PeerStatus.Online, "r1 down, r2 up")

	assert.Nil(t, srs1PeerStatus.PrimaryRoutes)
	assertNotNil(t, srs2PeerStatus.PrimaryRoutes)

	if srs2PeerStatus.PrimaryRoutes != nil {
		assert.Contains(
			t,
			srs2PeerStatus.PrimaryRoutes.AsSlice(),
			netip.MustParsePrefix(expectedRoutes[string(srs2.Self.ID)]),
		)
	}

	// Take down subnet router 2, leaving none available
	t.Logf("taking down subnet router r2 (%s)", subRouter2.Hostname())
	t.Logf("expecting r2 (%s) to remain primary, no other available", subRouter2.Hostname())
	err = subRouter2.Down()
	assertNoErr(t, err)

	time.Sleep(5 * time.Second)

	var routesAfterBothDown []*v1.Route
	err = executeAndUnmarshal(
		headscale,
		[]string{
			"headscale",
			"routes",
			"list",
			"--output",
			"json",
		},
		&routesAfterBothDown,
	)
	assertNoErr(t, err)
	assert.Len(t, routesAfterBothDown, 2)

	// Node 1 is not primary
	assert.Equal(t, true, routesAfterBothDown[0].GetAdvertised())
	assert.Equal(t, true, routesAfterBothDown[0].GetEnabled())
	assert.Equal(t, false, routesAfterBothDown[0].GetIsPrimary(), "r1 and r2 is down, expected r2 to _still_ be primary")

	// Node 2 is primary
	// if the node goes down, but no other suitable route is
	// available, keep the last known good route.
	assert.Equal(t, true, routesAfterBothDown[1].GetAdvertised())
	assert.Equal(t, true, routesAfterBothDown[1].GetEnabled())
	assert.Equal(t, true, routesAfterBothDown[1].GetIsPrimary(), "r1 and r2 is down, expected r2 to _still_ be primary")

	// TODO(kradalby): Check client status
	// Both are expected to be down

	// Verify that the route is not presented from either router
	clientStatus, err = client.Status()
	assertNoErr(t, err)

	srs1PeerStatus = clientStatus.Peer[srs1.Self.PublicKey]
	srs2PeerStatus = clientStatus.Peer[srs2.Self.PublicKey]

	assert.False(t, srs1PeerStatus.Online, "r1 down, r2 down")
	assert.False(t, srs2PeerStatus.Online, "r1 down, r2 down")

	assert.Nil(t, srs1PeerStatus.PrimaryRoutes)
	assertNotNil(t, srs2PeerStatus.PrimaryRoutes)

	if srs2PeerStatus.PrimaryRoutes != nil {
		assert.Contains(
			t,
			srs2PeerStatus.PrimaryRoutes.AsSlice(),
			netip.MustParsePrefix(expectedRoutes[string(srs2.Self.ID)]),
		)
	}

	// Bring up subnet router 1, making the route available from there.
	t.Logf("bringing up subnet router r1 (%s)", subRouter1.Hostname())
	t.Logf("expecting r1 (%s) to take over as primary (only one online)", subRouter1.Hostname())
	err = subRouter1.Up()
	assertNoErr(t, err)

	time.Sleep(5 * time.Second)

	var routesAfter1Up []*v1.Route
	err = executeAndUnmarshal(
		headscale,
		[]string{
			"headscale",
			"routes",
			"list",
			"--output",
			"json",
		},
		&routesAfter1Up,
	)
	assertNoErr(t, err)
	assert.Len(t, routesAfter1Up, 2)

	// Node 1 is primary
	assert.Equal(t, true, routesAfter1Up[0].GetAdvertised())
	assert.Equal(t, true, routesAfter1Up[0].GetEnabled())
	assert.Equal(t, true, routesAfter1Up[0].GetIsPrimary(), "r1 is back up, expected r1 to become be primary")

	// Node 2 is not primary
	assert.Equal(t, true, routesAfter1Up[1].GetAdvertised())
	assert.Equal(t, true, routesAfter1Up[1].GetEnabled())
	assert.Equal(t, false, routesAfter1Up[1].GetIsPrimary(), "r1 is back up, expected r1 to become be primary")

	// Verify that the route is announced from subnet router 1
	clientStatus, err = client.Status()
	assertNoErr(t, err)

	srs1PeerStatus = clientStatus.Peer[srs1.Self.PublicKey]
	srs2PeerStatus = clientStatus.Peer[srs2.Self.PublicKey]

	assert.True(t, srs1PeerStatus.Online, "r1 is back up, r2 down")
	assert.False(t, srs2PeerStatus.Online, "r1 is back up, r2 down")

	assert.NotNil(t, srs1PeerStatus.PrimaryRoutes)
	assert.Nil(t, srs2PeerStatus.PrimaryRoutes)

	if srs1PeerStatus.PrimaryRoutes != nil {
		assert.Contains(
			t,
			srs1PeerStatus.PrimaryRoutes.AsSlice(),
			netip.MustParsePrefix(expectedRoutes[string(srs1.Self.ID)]),
		)
	}

	// Bring up subnet router 2, should result in no change.
	t.Logf("bringing up subnet router r2 (%s)", subRouter2.Hostname())
	t.Logf("both online, expecting r1 (%s) to still be primary (no flapping)", subRouter1.Hostname())
	err = subRouter2.Up()
	assertNoErr(t, err)

	time.Sleep(5 * time.Second)

	var routesAfter2Up []*v1.Route
	err = executeAndUnmarshal(
		headscale,
		[]string{
			"headscale",
			"routes",
			"list",
			"--output",
			"json",
		},
		&routesAfter2Up,
	)
	assertNoErr(t, err)
	assert.Len(t, routesAfter2Up, 2)

	// Node 1 is not primary
	assert.Equal(t, true, routesAfter2Up[0].GetAdvertised())
	assert.Equal(t, true, routesAfter2Up[0].GetEnabled())
	assert.Equal(t, true, routesAfter2Up[0].GetIsPrimary(), "r1 and r2 is back up, expected r1 to _still_ be primary")

	// Node 2 is primary
	assert.Equal(t, true, routesAfter2Up[1].GetAdvertised())
	assert.Equal(t, true, routesAfter2Up[1].GetEnabled())
	assert.Equal(t, false, routesAfter2Up[1].GetIsPrimary(), "r1 and r2 is back up, expected r1 to _still_ be primary")

	// Verify that the route is announced from subnet router 1
	clientStatus, err = client.Status()
	assertNoErr(t, err)

	srs1PeerStatus = clientStatus.Peer[srs1.Self.PublicKey]
	srs2PeerStatus = clientStatus.Peer[srs2.Self.PublicKey]

	assert.True(t, srs1PeerStatus.Online, "r1 up, r2 up")
	assert.True(t, srs2PeerStatus.Online, "r1 up, r2 up")

	assert.NotNil(t, srs1PeerStatus.PrimaryRoutes)
	assert.Nil(t, srs2PeerStatus.PrimaryRoutes)

	if srs1PeerStatus.PrimaryRoutes != nil {
		assert.Contains(
			t,
			srs1PeerStatus.PrimaryRoutes.AsSlice(),
			netip.MustParsePrefix(expectedRoutes[string(srs1.Self.ID)]),
		)
	}

	// Disable the route of subnet router 1, making it failover to 2
	t.Logf("disabling route in subnet router r1 (%s)", subRouter1.Hostname())
	t.Logf("expecting route to failover to r2 (%s), which is still available", subRouter2.Hostname())
	_, err = headscale.Execute(
		[]string{
			"headscale",
			"routes",
			"disable",
			"--route",
			fmt.Sprintf("%d", routesAfter2Up[0].GetId()),
		})
	assertNoErr(t, err)

	time.Sleep(5 * time.Second)

	var routesAfterDisabling1 []*v1.Route
	err = executeAndUnmarshal(
		headscale,
		[]string{
			"headscale",
			"routes",
			"list",
			"--output",
			"json",
		},
		&routesAfterDisabling1,
	)
	assertNoErr(t, err)
	assert.Len(t, routesAfterDisabling1, 2)

	t.Logf("routes after disabling r1 %#v", routesAfterDisabling1)

	// Node 1 is not primary
	assert.Equal(t, true, routesAfterDisabling1[0].GetAdvertised())
	assert.Equal(t, false, routesAfterDisabling1[0].GetEnabled())
	assert.Equal(t, false, routesAfterDisabling1[0].GetIsPrimary())

	// Node 2 is primary
	assert.Equal(t, true, routesAfterDisabling1[1].GetAdvertised())
	assert.Equal(t, true, routesAfterDisabling1[1].GetEnabled())
	assert.Equal(t, true, routesAfterDisabling1[1].GetIsPrimary())

	// Verify that the route is announced from subnet router 1
	clientStatus, err = client.Status()
	assertNoErr(t, err)

	srs1PeerStatus = clientStatus.Peer[srs1.Self.PublicKey]
	srs2PeerStatus = clientStatus.Peer[srs2.Self.PublicKey]

	assert.Nil(t, srs1PeerStatus.PrimaryRoutes)
	assert.NotNil(t, srs2PeerStatus.PrimaryRoutes)

	if srs2PeerStatus.PrimaryRoutes != nil {
		assert.Contains(
			t,
			srs2PeerStatus.PrimaryRoutes.AsSlice(),
			netip.MustParsePrefix(expectedRoutes[string(srs2.Self.ID)]),
		)
	}

	// enable the route of subnet router 1, no change expected
	t.Logf("enabling route in subnet router 1 (%s)", subRouter1.Hostname())
	t.Logf("both online, expecting r2 (%s) to still be primary (no flapping)", subRouter2.Hostname())
	_, err = headscale.Execute(
		[]string{
			"headscale",
			"routes",
			"enable",
			"--route",
			fmt.Sprintf("%d", routesAfter2Up[0].GetId()),
		})
	assertNoErr(t, err)

	time.Sleep(5 * time.Second)

	var routesAfterEnabling1 []*v1.Route
	err = executeAndUnmarshal(
		headscale,
		[]string{
			"headscale",
			"routes",
			"list",
			"--output",
			"json",
		},
		&routesAfterEnabling1,
	)
	assertNoErr(t, err)
	assert.Len(t, routesAfterEnabling1, 2)

	// Node 1 is not primary
	assert.Equal(t, true, routesAfterEnabling1[0].GetAdvertised())
	assert.Equal(t, true, routesAfterEnabling1[0].GetEnabled())
	assert.Equal(t, false, routesAfterEnabling1[0].GetIsPrimary())

	// Node 2 is primary
	assert.Equal(t, true, routesAfterEnabling1[1].GetAdvertised())
	assert.Equal(t, true, routesAfterEnabling1[1].GetEnabled())
	assert.Equal(t, true, routesAfterEnabling1[1].GetIsPrimary())

	// Verify that the route is announced from subnet router 1
	clientStatus, err = client.Status()
	assertNoErr(t, err)

	srs1PeerStatus = clientStatus.Peer[srs1.Self.PublicKey]
	srs2PeerStatus = clientStatus.Peer[srs2.Self.PublicKey]

	assert.Nil(t, srs1PeerStatus.PrimaryRoutes)
	assert.NotNil(t, srs2PeerStatus.PrimaryRoutes)

	if srs2PeerStatus.PrimaryRoutes != nil {
		assert.Contains(
			t,
			srs2PeerStatus.PrimaryRoutes.AsSlice(),
			netip.MustParsePrefix(expectedRoutes[string(srs2.Self.ID)]),
		)
	}

	// delete the route of subnet router 2, failover to one expected
	t.Logf("deleting route in subnet router r2 (%s)", subRouter2.Hostname())
	t.Logf("expecting route to failover to r1 (%s)", subRouter1.Hostname())
	_, err = headscale.Execute(
		[]string{
			"headscale",
			"routes",
			"delete",
			"--route",
			fmt.Sprintf("%d", routesAfterEnabling1[1].GetId()),
		})
	assertNoErr(t, err)

	time.Sleep(5 * time.Second)

	var routesAfterDeleting2 []*v1.Route
	err = executeAndUnmarshal(
		headscale,
		[]string{
			"headscale",
			"routes",
			"list",
			"--output",
			"json",
		},
		&routesAfterDeleting2,
	)
	assertNoErr(t, err)
	assert.Len(t, routesAfterDeleting2, 1)

	t.Logf("routes after deleting r2 %#v", routesAfterDeleting2)

	// Node 1 is primary
	assert.Equal(t, true, routesAfterDeleting2[0].GetAdvertised())
	assert.Equal(t, true, routesAfterDeleting2[0].GetEnabled())
	assert.Equal(t, true, routesAfterDeleting2[0].GetIsPrimary())

	// Verify that the route is announced from subnet router 1
	clientStatus, err = client.Status()
	assertNoErr(t, err)

	srs1PeerStatus = clientStatus.Peer[srs1.Self.PublicKey]
	srs2PeerStatus = clientStatus.Peer[srs2.Self.PublicKey]

	assertNotNil(t, srs1PeerStatus.PrimaryRoutes)
	assert.Nil(t, srs2PeerStatus.PrimaryRoutes)

	if srs1PeerStatus.PrimaryRoutes != nil {
		assert.Contains(
			t,
			srs1PeerStatus.PrimaryRoutes.AsSlice(),
			netip.MustParsePrefix(expectedRoutes[string(srs1.Self.ID)]),
		)
	}
}

func TestEnableDisableAutoApprovedRoute(t *testing.T) {
	IntegrationSkip(t)
	t.Parallel()

	expectedRoutes := "172.0.0.0/24"

	user := "enable-disable-routing"

	scenario, err := NewScenario(dockertestMaxWait())
	assertNoErrf(t, "failed to create scenario: %s", err)
	defer scenario.Shutdown()

	spec := map[string]int{
		user: 1,
	}

	err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{tsic.WithTags([]string{"tag:approve"})}, hsic.WithTestName("clienableroute"), hsic.WithACLPolicy(
		&policy.ACLPolicy{
			ACLs: []policy.ACL{
				{
					Action:       "accept",
					Sources:      []string{"*"},
					Destinations: []string{"*:*"},
				},
			},
			TagOwners: map[string][]string{
				"tag:approve": {user},
			},
			AutoApprovers: policy.AutoApprovers{
				Routes: map[string][]string{
					expectedRoutes: {"tag:approve"},
				},
			},
		},
	))
	assertNoErrHeadscaleEnv(t, err)

	allClients, err := scenario.ListTailscaleClients()
	assertNoErrListClients(t, err)

	err = scenario.WaitForTailscaleSync()
	assertNoErrSync(t, err)

	headscale, err := scenario.Headscale()
	assertNoErrGetHeadscale(t, err)

	subRouter1 := allClients[0]

	// Initially advertise route
	command := []string{
		"tailscale",
		"set",
		"--advertise-routes=" + expectedRoutes,
	}
	_, _, err = subRouter1.Execute(command)
	assertNoErrf(t, "failed to advertise route: %s", err)

	time.Sleep(10 * time.Second)

	var routes []*v1.Route
	err = executeAndUnmarshal(
		headscale,
		[]string{
			"headscale",
			"routes",
			"list",
			"--output",
			"json",
		},
		&routes,
	)
	assertNoErr(t, err)
	assert.Len(t, routes, 1)

	// All routes should be auto approved and enabled
	assert.Equal(t, true, routes[0].GetAdvertised())
	assert.Equal(t, true, routes[0].GetEnabled())
	assert.Equal(t, true, routes[0].GetIsPrimary())

	// Stop advertising route
	command = []string{
		"tailscale",
		"set",
		"--advertise-routes=",
	}
	_, _, err = subRouter1.Execute(command)
	assertNoErrf(t, "failed to remove advertised route: %s", err)

	time.Sleep(10 * time.Second)

	var notAdvertisedRoutes []*v1.Route
	err = executeAndUnmarshal(
		headscale,
		[]string{
			"headscale",
			"routes",
			"list",
			"--output",
			"json",
		},
		&notAdvertisedRoutes,
	)
	assertNoErr(t, err)
	assert.Len(t, notAdvertisedRoutes, 1)

	// Route is no longer advertised
	assert.Equal(t, false, notAdvertisedRoutes[0].GetAdvertised())
	assert.Equal(t, false, notAdvertisedRoutes[0].GetEnabled())
	assert.Equal(t, true, notAdvertisedRoutes[0].GetIsPrimary())

	// Advertise route again
	command = []string{
		"tailscale",
		"set",
		"--advertise-routes=" + expectedRoutes,
	}
	_, _, err = subRouter1.Execute(command)
	assertNoErrf(t, "failed to advertise route: %s", err)

	time.Sleep(10 * time.Second)

	var reAdvertisedRoutes []*v1.Route
	err = executeAndUnmarshal(
		headscale,
		[]string{
			"headscale",
			"routes",
			"list",
			"--output",
			"json",
		},
		&reAdvertisedRoutes,
	)
	assertNoErr(t, err)
	assert.Len(t, reAdvertisedRoutes, 1)

	// All routes should be auto approved and enabled
	assert.Equal(t, true, reAdvertisedRoutes[0].GetAdvertised())
	assert.Equal(t, true, reAdvertisedRoutes[0].GetEnabled())
	assert.Equal(t, true, reAdvertisedRoutes[0].GetIsPrimary())
}

func TestAutoApprovedSubRoute2068(t *testing.T) {
	IntegrationSkip(t)
	t.Parallel()

	expectedRoutes := "10.42.7.0/24"

	user := "subroute"

	scenario, err := NewScenario(dockertestMaxWait())
	assertNoErrf(t, "failed to create scenario: %s", err)
	defer scenario.Shutdown()

	spec := map[string]int{
		user: 1,
	}

	err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{tsic.WithTags([]string{"tag:approve"})}, hsic.WithTestName("clienableroute"), hsic.WithACLPolicy(
		&policy.ACLPolicy{
			ACLs: []policy.ACL{
				{
					Action:       "accept",
					Sources:      []string{"*"},
					Destinations: []string{"*:*"},
				},
			},
			TagOwners: map[string][]string{
				"tag:approve": {user},
			},
			AutoApprovers: policy.AutoApprovers{
				Routes: map[string][]string{
					"10.42.0.0/16": {"tag:approve"},
				},
			},
		},
	))
	assertNoErrHeadscaleEnv(t, err)

	allClients, err := scenario.ListTailscaleClients()
	assertNoErrListClients(t, err)

	err = scenario.WaitForTailscaleSync()
	assertNoErrSync(t, err)

	headscale, err := scenario.Headscale()
	assertNoErrGetHeadscale(t, err)

	subRouter1 := allClients[0]

	// Initially advertise route
	command := []string{
		"tailscale",
		"set",
		"--advertise-routes=" + expectedRoutes,
	}
	_, _, err = subRouter1.Execute(command)
	assertNoErrf(t, "failed to advertise route: %s", err)

	time.Sleep(10 * time.Second)

	var routes []*v1.Route
	err = executeAndUnmarshal(
		headscale,
		[]string{
			"headscale",
			"routes",
			"list",
			"--output",
			"json",
		},
		&routes,
	)
	assertNoErr(t, err)
	assert.Len(t, routes, 1)

	want := []*v1.Route{
		{
			Id:         1,
			Prefix:     expectedRoutes,
			Advertised: true,
			Enabled:    true,
			IsPrimary:  true,
		},
	}

	if diff := cmp.Diff(want, routes, cmpopts.IgnoreUnexported(v1.Route{}), cmpopts.IgnoreFields(v1.Route{}, "Node", "CreatedAt", "UpdatedAt", "DeletedAt")); diff != "" {
		t.Errorf("unexpected routes (-want +got):\n%s", diff)
	}
}

// TestSubnetRouteACL verifies that Subnet routes are distributed
// as expected when ACLs are activated.
// It implements the issue from
// https://github.com/juanfont/headscale/issues/1604
func TestSubnetRouteACL(t *testing.T) {
	IntegrationSkip(t)
	t.Parallel()

	user := "subnet-route-acl"

	scenario, err := NewScenario(dockertestMaxWait())
	assertNoErrf(t, "failed to create scenario: %s", err)
	defer scenario.Shutdown()

	spec := map[string]int{
		user: 2,
	}

	err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("clienableroute"), hsic.WithACLPolicy(
		&policy.ACLPolicy{
			Groups: policy.Groups{
				"group:admins": {user},
			},
			ACLs: []policy.ACL{
				{
					Action:       "accept",
					Sources:      []string{"group:admins"},
					Destinations: []string{"group:admins:*"},
				},
				{
					Action:       "accept",
					Sources:      []string{"group:admins"},
					Destinations: []string{"10.33.0.0/16:*"},
				},
				// {
				// 	Action:       "accept",
				// 	Sources:      []string{"group:admins"},
				// 	Destinations: []string{"0.0.0.0/0:*"},
				// },
			},
		},
	))
	assertNoErrHeadscaleEnv(t, err)

	allClients, err := scenario.ListTailscaleClients()
	assertNoErrListClients(t, err)

	err = scenario.WaitForTailscaleSync()
	assertNoErrSync(t, err)

	headscale, err := scenario.Headscale()
	assertNoErrGetHeadscale(t, err)

	expectedRoutes := map[string]string{
		"1": "10.33.0.0/16",
	}

	// Sort nodes by ID
	sort.SliceStable(allClients, func(i, j int) bool {
		statusI, err := allClients[i].Status()
		if err != nil {
			return false
		}

		statusJ, err := allClients[j].Status()
		if err != nil {
			return false
		}

		return statusI.Self.ID < statusJ.Self.ID
	})

	subRouter1 := allClients[0]

	client := allClients[1]

	// advertise HA route on node 1 and 2
	// ID 1 will be primary
	// ID 2 will be secondary
	for _, client := range allClients {
		status, err := client.Status()
		assertNoErr(t, err)

		if route, ok := expectedRoutes[string(status.Self.ID)]; ok {
			command := []string{
				"tailscale",
				"set",
				"--advertise-routes=" + route,
			}
			_, _, err = client.Execute(command)
			assertNoErrf(t, "failed to advertise route: %s", err)
		}
	}

	err = scenario.WaitForTailscaleSync()
	assertNoErrSync(t, err)

	var routes []*v1.Route
	err = executeAndUnmarshal(
		headscale,
		[]string{
			"headscale",
			"routes",
			"list",
			"--output",
			"json",
		},
		&routes,
	)

	assertNoErr(t, err)
	assert.Len(t, routes, 1)

	for _, route := range routes {
		assert.Equal(t, true, route.GetAdvertised())
		assert.Equal(t, false, route.GetEnabled())
		assert.Equal(t, false, route.GetIsPrimary())
	}

	// Verify that no routes has been sent to the client,
	// they are not yet enabled.
	for _, client := range allClients {
		status, err := client.Status()
		assertNoErr(t, err)

		for _, peerKey := range status.Peers() {
			peerStatus := status.Peer[peerKey]

			assert.Nil(t, peerStatus.PrimaryRoutes)
		}
	}

	// Enable all routes
	for _, route := range routes {
		_, err = headscale.Execute(
			[]string{
				"headscale",
				"routes",
				"enable",
				"--route",
				strconv.Itoa(int(route.GetId())),
			})
		assertNoErr(t, err)
	}

	time.Sleep(5 * time.Second)

	var enablingRoutes []*v1.Route
	err = executeAndUnmarshal(
		headscale,
		[]string{
			"headscale",
			"routes",
			"list",
			"--output",
			"json",
		},
		&enablingRoutes,
	)
	assertNoErr(t, err)
	assert.Len(t, enablingRoutes, 1)

	// Node 1 has active route
	assert.Equal(t, true, enablingRoutes[0].GetAdvertised())
	assert.Equal(t, true, enablingRoutes[0].GetEnabled())
	assert.Equal(t, true, enablingRoutes[0].GetIsPrimary())

	// Verify that the client has routes from the primary machine
	srs1, _ := subRouter1.Status()

	clientStatus, err := client.Status()
	assertNoErr(t, err)

	srs1PeerStatus := clientStatus.Peer[srs1.Self.PublicKey]

	assertNotNil(t, srs1PeerStatus.PrimaryRoutes)

	t.Logf("subnet1 has following routes: %v", srs1PeerStatus.PrimaryRoutes.AsSlice())
	assert.Len(t, srs1PeerStatus.PrimaryRoutes.AsSlice(), 1)
	assert.Contains(
		t,
		srs1PeerStatus.PrimaryRoutes.AsSlice(),
		netip.MustParsePrefix(expectedRoutes[string(srs1.Self.ID)]),
	)

	clientNm, err := client.Netmap()
	assertNoErr(t, err)

	wantClientFilter := []filter.Match{
		{
			IPProto: views.SliceOf([]ipproto.Proto{
				ipproto.TCP, ipproto.UDP, ipproto.ICMPv4, ipproto.ICMPv6,
			}),
			Srcs: []netip.Prefix{
				netip.MustParsePrefix("100.64.0.1/32"),
				netip.MustParsePrefix("100.64.0.2/32"),
				netip.MustParsePrefix("fd7a:115c:a1e0::1/128"),
				netip.MustParsePrefix("fd7a:115c:a1e0::2/128"),
			},
			Dsts: []filter.NetPortRange{
				{
					Net:   netip.MustParsePrefix("100.64.0.2/32"),
					Ports: filter.PortRange{0, 0xffff},
				},
				{
					Net:   netip.MustParsePrefix("fd7a:115c:a1e0::2/128"),
					Ports: filter.PortRange{0, 0xffff},
				},
			},
			Caps: []filter.CapMatch{},
		},
	}

	if diff := cmp.Diff(wantClientFilter, clientNm.PacketFilter, util.ViewSliceIPProtoComparer, util.PrefixComparer); diff != "" {
		t.Errorf("Client (%s) filter, unexpected result (-want +got):\n%s", client.Hostname(), diff)
	}

	subnetNm, err := subRouter1.Netmap()
	assertNoErr(t, err)

	wantSubnetFilter := []filter.Match{
		{
			IPProto: views.SliceOf([]ipproto.Proto{
				ipproto.TCP, ipproto.UDP, ipproto.ICMPv4, ipproto.ICMPv6,
			}),
			Srcs: []netip.Prefix{
				netip.MustParsePrefix("100.64.0.1/32"),
				netip.MustParsePrefix("100.64.0.2/32"),
				netip.MustParsePrefix("fd7a:115c:a1e0::1/128"),
				netip.MustParsePrefix("fd7a:115c:a1e0::2/128"),
			},
			Dsts: []filter.NetPortRange{
				{
					Net:   netip.MustParsePrefix("100.64.0.1/32"),
					Ports: filter.PortRange{0, 0xffff},
				},
				{
					Net:   netip.MustParsePrefix("fd7a:115c:a1e0::1/128"),
					Ports: filter.PortRange{0, 0xffff},
				},
			},
			Caps: []filter.CapMatch{},
		},
		{
			IPProto: views.SliceOf([]ipproto.Proto{
				ipproto.TCP, ipproto.UDP, ipproto.ICMPv4, ipproto.ICMPv6,
			}),
			Srcs: []netip.Prefix{
				netip.MustParsePrefix("100.64.0.1/32"),
				netip.MustParsePrefix("100.64.0.2/32"),
				netip.MustParsePrefix("fd7a:115c:a1e0::1/128"),
				netip.MustParsePrefix("fd7a:115c:a1e0::2/128"),
			},
			Dsts: []filter.NetPortRange{
				{
					Net:   netip.MustParsePrefix("10.33.0.0/16"),
					Ports: filter.PortRange{0, 0xffff},
				},
			},
			Caps: []filter.CapMatch{},
		},
	}

	if diff := cmp.Diff(wantSubnetFilter, subnetNm.PacketFilter, util.ViewSliceIPProtoComparer, util.PrefixComparer); diff != "" {
		t.Errorf("Subnet (%s) filter, unexpected result (-want +got):\n%s", subRouter1.Hostname(), diff)
	}
}