package policy

import (
	"fmt"
	"net/netip"
	"testing"

	policyv2 "github.com/juanfont/headscale/hscontrol/policy/v2"
	"github.com/juanfont/headscale/hscontrol/types"
	"github.com/juanfont/headscale/hscontrol/util"
	"github.com/stretchr/testify/assert"
	"gorm.io/gorm"
	"tailscale.com/net/tsaddr"
	"tailscale.com/types/key"
	"tailscale.com/types/ptr"
	"tailscale.com/types/views"
)

func TestApproveRoutesWithPolicy_NeverRemovesApprovedRoutes(t *testing.T) {
	user1 := types.User{
		Model: gorm.Model{ID: 1},
		Name:  "testuser@",
	}
	user2 := types.User{
		Model: gorm.Model{ID: 2},
		Name:  "otheruser@",
	}
	users := []types.User{user1, user2}

	node1 := &types.Node{
		ID:             1,
		MachineKey:     key.NewMachine().Public(),
		NodeKey:        key.NewNode().Public(),
		Hostname:       "test-node",
		UserID:         user1.ID,
		User:           user1,
		RegisterMethod: util.RegisterMethodAuthKey,
		IPv4:           ptr.To(netip.MustParseAddr("100.64.0.1")),
		ForcedTags:     []string{"tag:test"},
	}

	node2 := &types.Node{
		ID:             2,
		MachineKey:     key.NewMachine().Public(),
		NodeKey:        key.NewNode().Public(),
		Hostname:       "other-node",
		UserID:         user2.ID,
		User:           user2,
		RegisterMethod: util.RegisterMethodAuthKey,
		IPv4:           ptr.To(netip.MustParseAddr("100.64.0.2")),
	}

	// Create a policy that auto-approves specific routes
	policyJSON := `{
		"groups": {
			"group:test": ["testuser@"]
		},
		"tagOwners": {
			"tag:test": ["testuser@"]
		},
		"acls": [
			{
				"action": "accept",
				"src": ["*"],
				"dst": ["*:*"]
			}
		],
		"autoApprovers": {
			"routes": {
				"10.0.0.0/8": ["testuser@", "tag:test"],
				"10.1.0.0/24": ["testuser@"],
				"10.2.0.0/24": ["testuser@"],
				"192.168.0.0/24": ["tag:test"]
			}
		}
	}`

	pm, err := policyv2.NewPolicyManager([]byte(policyJSON), users, views.SliceOf([]types.NodeView{node1.View(), node2.View()}))
	assert.NoError(t, err)

	tests := []struct {
		name            string
		node            *types.Node
		currentApproved []netip.Prefix
		announcedRoutes []netip.Prefix
		wantApproved    []netip.Prefix
		wantChanged     bool
		description     string
	}{
		{
			name: "previously_approved_route_no_longer_advertised_should_remain",
			node: node1,
			currentApproved: []netip.Prefix{
				netip.MustParsePrefix("10.0.0.0/24"),
				netip.MustParsePrefix("192.168.0.0/24"),
			},
			announcedRoutes: []netip.Prefix{
				netip.MustParsePrefix("10.0.0.0/24"), // Only this one is still advertised
			},
			wantApproved: []netip.Prefix{
				netip.MustParsePrefix("10.0.0.0/24"),
				netip.MustParsePrefix("192.168.0.0/24"), // Should still be here!
			},
			wantChanged: false,
			description: "Previously approved routes should never be removed even when no longer advertised",
		},
		{
			name: "add_new_auto_approved_route_keeps_old_approved",
			node: node1,
			currentApproved: []netip.Prefix{
				netip.MustParsePrefix("10.5.0.0/24"), // This was manually approved
			},
			announcedRoutes: []netip.Prefix{
				netip.MustParsePrefix("10.1.0.0/24"), // New route that should be auto-approved
			},
			wantApproved: []netip.Prefix{
				netip.MustParsePrefix("10.1.0.0/24"), // New auto-approved route (subset of 10.0.0.0/8)
				netip.MustParsePrefix("10.5.0.0/24"), // Old approved route kept
			},
			wantChanged: true,
			description: "New auto-approved routes should be added while keeping old approved routes",
		},
		{
			name: "no_announced_routes_keeps_all_approved",
			node: node1,
			currentApproved: []netip.Prefix{
				netip.MustParsePrefix("10.0.0.0/24"),
				netip.MustParsePrefix("192.168.0.0/24"),
				netip.MustParsePrefix("172.16.0.0/16"),
			},
			announcedRoutes: []netip.Prefix{}, // No routes announced
			wantApproved: []netip.Prefix{
				netip.MustParsePrefix("10.0.0.0/24"),
				netip.MustParsePrefix("172.16.0.0/16"),
				netip.MustParsePrefix("192.168.0.0/24"),
			},
			wantChanged: false,
			description: "All approved routes should remain when no routes are announced",
		},
		{
			name: "no_changes_when_announced_equals_approved",
			node: node1,
			currentApproved: []netip.Prefix{
				netip.MustParsePrefix("10.0.0.0/24"),
			},
			announcedRoutes: []netip.Prefix{
				netip.MustParsePrefix("10.0.0.0/24"),
			},
			wantApproved: []netip.Prefix{
				netip.MustParsePrefix("10.0.0.0/24"),
			},
			wantChanged: false,
			description: "No changes should occur when announced routes match approved routes",
		},
		{
			name: "auto_approve_multiple_new_routes",
			node: node1,
			currentApproved: []netip.Prefix{
				netip.MustParsePrefix("172.16.0.0/24"), // This was manually approved
			},
			announcedRoutes: []netip.Prefix{
				netip.MustParsePrefix("10.2.0.0/24"),    // Should be auto-approved (subset of 10.0.0.0/8)
				netip.MustParsePrefix("192.168.0.0/24"), // Should be auto-approved for tag:test
			},
			wantApproved: []netip.Prefix{
				netip.MustParsePrefix("10.2.0.0/24"),    // New auto-approved
				netip.MustParsePrefix("172.16.0.0/24"),  // Original kept
				netip.MustParsePrefix("192.168.0.0/24"), // New auto-approved
			},
			wantChanged: true,
			description: "Multiple new routes should be auto-approved while keeping existing approved routes",
		},
		{
			name: "node_without_permission_no_auto_approval",
			node: node2, // Different node without the tag
			currentApproved: []netip.Prefix{
				netip.MustParsePrefix("10.0.0.0/24"),
			},
			announcedRoutes: []netip.Prefix{
				netip.MustParsePrefix("192.168.0.0/24"), // This requires tag:test
			},
			wantApproved: []netip.Prefix{
				netip.MustParsePrefix("10.0.0.0/24"), // Only the original approved route
			},
			wantChanged: false,
			description: "Routes should not be auto-approved for nodes without proper permissions",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			gotApproved, gotChanged := ApproveRoutesWithPolicy(pm, tt.node.View(), tt.currentApproved, tt.announcedRoutes)

			assert.Equal(t, tt.wantChanged, gotChanged, "changed flag mismatch: %s", tt.description)

			// Sort for comparison since ApproveRoutesWithPolicy sorts the results
			tsaddr.SortPrefixes(tt.wantApproved)
			assert.Equal(t, tt.wantApproved, gotApproved, "approved routes mismatch: %s", tt.description)

			// Verify that all previously approved routes are still present
			for _, prevRoute := range tt.currentApproved {
				assert.Contains(t, gotApproved, prevRoute,
					"previously approved route %s was removed - this should never happen", prevRoute)
			}
		})
	}
}

func TestApproveRoutesWithPolicy_NilAndEmptyCases(t *testing.T) {
	// Create a basic policy for edge case testing
	aclPolicy := `
{
	"acls": [
		{"action": "accept", "src": ["*"], "dst": ["*:*"]},
	],
	"autoApprovers": {
		"routes": {
			"10.1.0.0/24": ["test@"],
		},
	},
}`

	pmfs := PolicyManagerFuncsForTest([]byte(aclPolicy))

	tests := []struct {
		name            string
		currentApproved []netip.Prefix
		announcedRoutes []netip.Prefix
		wantApproved    []netip.Prefix
		wantChanged     bool
	}{
		{
			name: "nil_policy_manager",
			currentApproved: []netip.Prefix{
				netip.MustParsePrefix("10.0.0.0/24"),
			},
			announcedRoutes: []netip.Prefix{
				netip.MustParsePrefix("192.168.0.0/24"),
			},
			wantApproved: []netip.Prefix{
				netip.MustParsePrefix("10.0.0.0/24"),
			},
			wantChanged: false,
		},
		{
			name:            "nil_current_approved",
			currentApproved: nil,
			announcedRoutes: []netip.Prefix{
				netip.MustParsePrefix("10.1.0.0/24"),
			},
			wantApproved: []netip.Prefix{
				netip.MustParsePrefix("10.1.0.0/24"),
			},
			wantChanged: true,
		},
		{
			name: "nil_announced_routes",
			currentApproved: []netip.Prefix{
				netip.MustParsePrefix("10.0.0.0/24"),
			},
			announcedRoutes: nil,
			wantApproved: []netip.Prefix{
				netip.MustParsePrefix("10.0.0.0/24"),
			},
			wantChanged: false,
		},
		{
			name: "duplicate_approved_routes",
			currentApproved: []netip.Prefix{
				netip.MustParsePrefix("10.0.0.0/24"),
				netip.MustParsePrefix("10.0.0.0/24"), // Duplicate
			},
			announcedRoutes: []netip.Prefix{
				netip.MustParsePrefix("10.1.0.0/24"),
			},
			wantApproved: []netip.Prefix{
				netip.MustParsePrefix("10.0.0.0/24"),
				netip.MustParsePrefix("10.1.0.0/24"),
			},
			wantChanged: true,
		},
		{
			name:            "empty_slices",
			currentApproved: []netip.Prefix{},
			announcedRoutes: []netip.Prefix{},
			wantApproved:    []netip.Prefix{},
			wantChanged:     false,
		},
	}

	for _, tt := range tests {
		for i, pmf := range pmfs {
			t.Run(fmt.Sprintf("%s-policy-index%d", tt.name, i), func(t *testing.T) {
				// Create test user
				user := types.User{
					Model: gorm.Model{ID: 1},
					Name:  "test",
				}
				users := []types.User{user}

				// Create test node
				node := types.Node{
					ID:             1,
					MachineKey:     key.NewMachine().Public(),
					NodeKey:        key.NewNode().Public(),
					Hostname:       "testnode",
					UserID:         user.ID,
					User:           user,
					RegisterMethod: util.RegisterMethodAuthKey,
					IPv4:           ptr.To(netip.MustParseAddr("100.64.0.1")),
					ApprovedRoutes: tt.currentApproved,
				}
				nodes := types.Nodes{&node}

				// Create policy manager or use nil if specified
				var pm PolicyManager
				var err error
				if tt.name != "nil_policy_manager" {
					pm, err = pmf(users, nodes.ViewSlice())
					assert.NoError(t, err)
				} else {
					pm = nil
				}

				gotApproved, gotChanged := ApproveRoutesWithPolicy(pm, node.View(), tt.currentApproved, tt.announcedRoutes)

				assert.Equal(t, tt.wantChanged, gotChanged, "changed flag mismatch")

				// Handle nil vs empty slice comparison
				if tt.wantApproved == nil {
					assert.Nil(t, gotApproved, "expected nil approved routes")
				} else {
					tsaddr.SortPrefixes(tt.wantApproved)
					assert.Equal(t, tt.wantApproved, gotApproved, "approved routes mismatch")
				}
			})
		}
	}
}
