package servertest_test

import (
	"net/netip"
	"testing"
	"time"

	"github.com/juanfont/headscale/hscontrol/servertest"
	"github.com/juanfont/headscale/hscontrol/types/change"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"tailscale.com/types/netmap"
)

// Dynamic HA failover scenarios, observed from a viewer client's
// perspective. Unlike the static [TestViaGrantHACompat] golden tests,
// these exercise runtime transitions: a primary going unhealthy,
// revoking its approved route, or losing its tag, and verify that
// the viewer's [netmap.NetworkMap] converges to the new primary. These are the
// end-to-end signals that static captures cannot cover.

// hasPeerPrimaryRoute reports whether the viewer's current [netmap.NetworkMap]
// lists route as a PrimaryRoute on the peer with the given hostname.
func hasPeerPrimaryRoute(nm *netmap.NetworkMap, peerHost string, route netip.Prefix) bool {
	if nm == nil {
		return false
	}

	for _, p := range nm.Peers {
		hi := p.Hostinfo()
		if !hi.Valid() || hi.Hostname() != peerHost {
			continue
		}

		for i := range p.PrimaryRoutes().Len() {
			if p.PrimaryRoutes().At(i) == route {
				return true
			}
		}
	}

	return false
}

// TestHAFailover_ViewerSeesPrimaryFlip verifies that when an HA
// primary is marked unhealthy, the viewer's [netmap.NetworkMap] flips the route's
// primary assignment from the old primary to the standby.
func TestHAFailover_ViewerSeesPrimaryFlip(t *testing.T) {
	t.Parallel()

	srv := servertest.NewServer(t)
	user := srv.CreateUser(t, "ha-dyn-flip")

	route := netip.MustParsePrefix("10.100.0.0/24")

	r1 := servertest.NewClient(t, srv, "dyn-flip-r1", servertest.WithUser(user))
	r2 := servertest.NewClient(t, srv, "dyn-flip-r2", servertest.WithUser(user))
	viewer := servertest.NewClient(t, srv, "dyn-flip-view", servertest.WithUser(user))

	r1.WaitForPeers(t, 2, 10*time.Second)
	r2.WaitForPeers(t, 2, 10*time.Second)
	viewer.WaitForPeers(t, 2, 10*time.Second)

	id1 := advertiseAndApproveRoute(t, srv, r1, route)
	id2 := advertiseAndApproveRoute(t, srv, r2, route)

	require.Contains(t, srv.State().GetNodePrimaryRoutes(id1), route,
		"node 1 should be primary initially")

	viewer.WaitForCondition(t, "viewer sees route via r1", 10*time.Second,
		func(nm *netmap.NetworkMap) bool {
			return hasPeerPrimaryRoute(nm, "dyn-flip-r1", route)
		})

	changed := srv.State().SetNodeHealth(id1, false)
	require.True(t, changed, "marking primary unhealthy should change primaries")

	srv.App.Change(change.PolicyChange())

	assert.Contains(t, srv.State().GetNodePrimaryRoutes(id2), route,
		"node 2 should be primary after failover")
	assert.NotContains(t, srv.State().GetNodePrimaryRoutes(id1), route,
		"node 1 should no longer be primary")

	viewer.WaitForCondition(t, "viewer sees route via r2", 10*time.Second,
		func(nm *netmap.NetworkMap) bool {
			return hasPeerPrimaryRoute(nm, "dyn-flip-r2", route) &&
				!hasPeerPrimaryRoute(nm, "dyn-flip-r1", route)
		})
}

// TestHAFailover_ViewerSeesRouteRevoke verifies that when the primary
// revokes its approved route, the viewer's [netmap.NetworkMap] re-elects the
// standby and the old primary no longer advertises the route.
func TestHAFailover_ViewerSeesRouteRevoke(t *testing.T) {
	t.Parallel()

	srv := servertest.NewServer(t)
	user := srv.CreateUser(t, "ha-dyn-revoke")

	route := netip.MustParsePrefix("10.101.0.0/24")

	r1 := servertest.NewClient(t, srv, "dyn-rev-r1", servertest.WithUser(user))
	r2 := servertest.NewClient(t, srv, "dyn-rev-r2", servertest.WithUser(user))
	viewer := servertest.NewClient(t, srv, "dyn-rev-view", servertest.WithUser(user))

	r1.WaitForPeers(t, 2, 10*time.Second)
	r2.WaitForPeers(t, 2, 10*time.Second)
	viewer.WaitForPeers(t, 2, 10*time.Second)

	id1 := advertiseAndApproveRoute(t, srv, r1, route)
	id2 := advertiseAndApproveRoute(t, srv, r2, route)

	viewer.WaitForCondition(t, "viewer sees route via r1", 10*time.Second,
		func(nm *netmap.NetworkMap) bool {
			return hasPeerPrimaryRoute(nm, "dyn-rev-r1", route)
		})

	_, rc, err := srv.State().SetApprovedRoutes(id1, nil)
	require.NoError(t, err, "revoking approved routes should succeed")

	srv.App.Change(rc)

	assert.NotContains(t, srv.State().GetNodePrimaryRoutes(id1), route,
		"node 1 should no longer be primary after revoke")
	assert.Contains(t, srv.State().GetNodePrimaryRoutes(id2), route,
		"node 2 should be primary after revoke")

	viewer.WaitForCondition(t, "viewer sees route via r2 after revoke", 10*time.Second,
		func(nm *netmap.NetworkMap) bool {
			return hasPeerPrimaryRoute(nm, "dyn-rev-r2", route) &&
				!hasPeerPrimaryRoute(nm, "dyn-rev-r1", route)
		})
}
