// Copyright 2026 Microsoft Corporation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package policy

import (
	"regexp"
	"testing"
	"time"

	"k8s.io/apimachinery/pkg/util/sets"

	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources"
)

func TestSelectsResourceGroup_IntendedLegacyPolicyBehavior(t *testing.T) {
	t.Parallel()

	now := time.Date(2026, time.March, 16, 15, 0, 0, 0, time.UTC)

	pol := &RGDiscoveryPolicy{
		Rules: []RGDiscoveryRule{
			{
				Name:   "skip-managed-resource-groups",
				Action: RGDiscoveryActionSkip,
				Match:  RGDiscoveryMatch{Any: true},
				Conditions: RGDiscoveryConditions{
					ManagedByAlive: boolPtr(true),
				},
			},
			{
				Name:   "pers-persist-delete-after-15d",
				Action: RGDiscoveryActionDelete,
				Match:  RGDiscoveryMatch{NamePrefix: "hcp-underlay-pers-"},
				Conditions: RGDiscoveryConditions{
					TagsEq: map[string]string{"persist": "true"},
				},
				OlderThan: 15 * 24 * time.Hour,
			},
			{
				Name:   "pers-non-persist-delete-after-2d",
				Action: RGDiscoveryActionDelete,
				Match:  RGDiscoveryMatch{NamePrefix: "hcp-underlay-pers-"},
				Conditions: RGDiscoveryConditions{
					TagsNotEq: map[string]string{"persist": "true"},
				},
				OlderThan: 48 * time.Hour,
			},
			{
				Name:   "skip-persist-true-globally",
				Action: RGDiscoveryActionSkip,
				Match:  RGDiscoveryMatch{Any: true},
				Conditions: RGDiscoveryConditions{
					TagsEq: map[string]string{"persist": "true"},
				},
			},
			{
				Name:      "global-default-delete-after-2d",
				Action:    RGDiscoveryActionDelete,
				Match:     RGDiscoveryMatch{Any: true},
				OlderThan: 48 * time.Hour,
			},
		},
	}

	testCases := []struct {
		name           string
		rg             *armresources.ResourceGroup
		excluded       sets.Set[string]
		knownRGs       sets.Set[string]
		expectSelected bool
	}{
		{
			name: "managed RG with alive target is always skipped",
			rg: newResourceGroupWithManagedBy(
				"hcp-underlay-pers-usw3rvaz",
				"/subscriptions/sub/resourceGroups/hcp-underlay-pers-usw3parent/providers/Microsoft.ContainerService/managedClusters/aks1",
			),
			knownRGs:       sets.New("hcp-underlay-pers-usw3parent"),
			expectSelected: false,
		},
		{
			name: "orphaned managed RG falls through to delete rules",
			rg: newResourceGroup(
				"hcp-underlay-pers-usw3rvaz",
				timePtr(now.Add(-72*time.Hour)),
				nil,
				true,
			),
			expectSelected: true,
		},
		{
			name: "pers persist true older than 15 days is selected",
			rg: newResourceGroup(
				"hcp-underlay-pers-usw3rvaz",
				timePtr(now.Add(-16*24*time.Hour)),
				map[string]string{"persist": "true"},
				false,
			),
			excluded:       sets.New[string](),
			expectSelected: true,
		},
		{
			name: "pers persist true younger than 15 days is skipped",
			rg: newResourceGroup(
				"hcp-underlay-pers-usw3rvaz",
				timePtr(now.Add(-10*24*time.Hour)),
				map[string]string{"persist": "true"},
				false,
			),
			excluded:       sets.New[string](),
			expectSelected: false,
		},
		{
			name: "pers persist false older than 2 days is selected",
			rg: newResourceGroup(
				"hcp-underlay-pers-usw3rvaz",
				timePtr(now.Add(-72*time.Hour)),
				map[string]string{"persist": "false"},
				false,
			),
			excluded:       sets.New[string](),
			expectSelected: true,
		},
		{
			name: "pers missing persist tag uses 2-day rule",
			rg: newResourceGroup(
				"hcp-underlay-pers-usw3rvaz",
				timePtr(now.Add(-72*time.Hour)),
				nil,
				false,
			),
			excluded:       sets.New[string](),
			expectSelected: true,
		},
		{
			name: "non-pers older than 2 days is selected by global default",
			rg: newResourceGroup(
				"hcp-underlay-dev-usw3rvaz",
				timePtr(now.Add(-72*time.Hour)),
				nil,
				false,
			),
			excluded:       sets.New[string](),
			expectSelected: true,
		},
		{
			name: "missing createdAt is skipped",
			rg: newResourceGroup(
				"hcp-underlay-dev-usw3rvaz",
				nil,
				nil,
				false,
			),
			excluded:       sets.New[string](),
			expectSelected: false,
		},
		{
			name: "excluded resource group is skipped regardless of age",
			rg: newResourceGroup(
				"hcp-underlay-dev-usw3rvaz",
				timePtr(now.Add(-72*time.Hour)),
				nil,
				false,
			),
			excluded:       sets.New("hcp-underlay-dev-usw3rvaz"),
			expectSelected: false,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			selected, _ := pol.SelectsResourceGroup(tc.rg, tc.excluded, tc.knownRGs, now)
			if selected != tc.expectSelected {
				t.Fatalf("expected selected=%t, got selected=%t", tc.expectSelected, selected)
			}
		})
	}
}

func TestParseCreatedAt_AzureFormat(t *testing.T) {
	t.Parallel()

	tags := map[string]*string{
		"createdAt": strPtr("2026-03-16T15:00:00.1234567Z"),
	}

	parsed, ok := parseCreatedAt(tags)
	if !ok {
		t.Fatalf("expected createdAt to parse")
	}

	expected := time.Date(2026, time.March, 16, 15, 0, 0, 123456700, time.UTC)
	if !parsed.Equal(expected) {
		t.Fatalf("expected parsed time %s, got %s", expected, parsed)
	}
}

func TestSelectsResourceGroup_ReturnsStructuredRuleReason(t *testing.T) {
	t.Parallel()

	now := time.Date(2026, time.March, 16, 15, 0, 0, 0, time.UTC)
	pol := &RGDiscoveryPolicy{
		Rules: []RGDiscoveryRule{
			{
				Name:      "global-default-delete-after-2d",
				Action:    RGDiscoveryActionDelete,
				Match:     RGDiscoveryMatch{Any: true},
				OlderThan: 48 * time.Hour,
			},
		},
	}
	rg := newResourceGroup("example-rg", timePtr(now.Add(-72*time.Hour)), nil, false)

	selected, reason := pol.SelectsResourceGroup(rg, sets.New[string](), sets.New[string](), now)
	if !selected {
		t.Fatalf("expected resource group to be selected")
	}
	if reason.Rule == nil {
		t.Fatalf("expected structured rule details in reason")
	}
	if reason.Rule.Index != 0 {
		t.Fatalf("expected rule index 0, got %d", reason.Rule.Index)
	}
	if reason.Rule.Name != "global-default-delete-after-2d" {
		t.Fatalf("unexpected rule name %q", reason.Rule.Name)
	}
	if reason.Rule.Result != "expired" {
		t.Fatalf("unexpected rule result %q", reason.Rule.Result)
	}
	if got, want := reason.String(), "rule[0]-expired"; got != want {
		t.Fatalf("expected %q, got %q", want, got)
	}
}

func TestSelectsResourceGroup_PersistTrueDeleteAfter15d(t *testing.T) {
	t.Parallel()

	now := time.Date(2026, time.March, 16, 15, 0, 0, 0, time.UTC)
	pol := &RGDiscoveryPolicy{
		Rules: []RGDiscoveryRule{
			{
				Name:   "skip-shared-env-persist-true",
				Action: RGDiscoveryActionSkip,
				Match:  RGDiscoveryMatch{NameRegex: regexp.MustCompile(`^hcp-underlay-(cspr|dev|perf|int|stg|prod)-`)},
				Conditions: RGDiscoveryConditions{
					TagsEq: map[string]string{"persist": "true"},
				},
			},
			{
				Name:   "persist-true-delete-after-15d",
				Action: RGDiscoveryActionDelete,
				Match:  RGDiscoveryMatch{Any: true},
				Conditions: RGDiscoveryConditions{
					TagsEq: map[string]string{"persist": "true"},
				},
				OlderThan: 15 * 24 * time.Hour,
			},
		},
	}

	excluded := sets.New("global", "dashboards", "opstool-westus3")

	testCases := []struct {
		name           string
		rg             *armresources.ResourceGroup
		expectSelected bool
	}{
		{
			name: "custom env swft persist true older than 15d is selected",
			rg: newResourceGroup(
				"hcp-underlay-swft-usw3rvaz",
				timePtr(now.Add(-16*24*time.Hour)),
				map[string]string{"persist": "true"},
				false,
			),
			expectSelected: true,
		},
		{
			name: "custom env swft persist true younger than 15d is not selected",
			rg: newResourceGroup(
				"hcp-underlay-swft-usw3rvaz",
				timePtr(now.Add(-10*24*time.Hour)),
				map[string]string{"persist": "true"},
				false,
			),
			expectSelected: false,
		},
		{
			name: "adhoc RG zgalor-test persist true older than 15d is selected",
			rg: newResourceGroup(
				"zgalor-test",
				timePtr(now.Add(-20*24*time.Hour)),
				map[string]string{"persist": "true"},
				false,
			),
			expectSelected: true,
		},
		{
			name: "adhoc RG bbergen-net-rg-03 persist true older than 15d is selected",
			rg: newResourceGroup(
				"bbergen-net-rg-03",
				timePtr(now.Add(-90*24*time.Hour)),
				map[string]string{"persist": "true"},
				false,
			),
			expectSelected: true,
		},
		{
			name: "shared env dev persist true is skipped",
			rg: newResourceGroup(
				"hcp-underlay-dev-usw3rvaz",
				timePtr(now.Add(-20*24*time.Hour)),
				map[string]string{"persist": "true"},
				false,
			),
			expectSelected: false,
		},
		{
			name: "shared env cspr persist true is skipped",
			rg: newResourceGroup(
				"hcp-underlay-cspr-usw3rvaz",
				timePtr(now.Add(-20*24*time.Hour)),
				map[string]string{"persist": "true"},
				false,
			),
			expectSelected: false,
		},
		{
			name: "excluded infra RG is skipped regardless of persist",
			rg: newResourceGroup(
				"dashboards",
				timePtr(now.Add(-90*24*time.Hour)),
				map[string]string{"persist": "true"},
				false,
			),
			expectSelected: false,
		},
		{
			name: "RG without persist does not match",
			rg: newResourceGroup(
				"zgalor-test",
				timePtr(now.Add(-72*time.Hour)),
				nil,
				false,
			),
			expectSelected: false,
		},
	}

	for _, tc := range testCases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			selected, _ := pol.SelectsResourceGroup(tc.rg, excluded, sets.New[string](), now)
			if selected != tc.expectSelected {
				t.Fatalf("expected selected=%t, got selected=%t", tc.expectSelected, selected)
			}
		})
	}
}

func TestSelectionReasonSourceDescription_WithAndWithoutRule(t *testing.T) {
	t.Parallel()

	withRule := RGSelectionReason{
		Rule: &RGSelectionRule{
			Index:  2,
			Name:   "named-rule",
			Result: "expired",
		},
	}
	if got, want := withRule.SourceDescription(), `matched policy rule "named-rule" (expired)`; got != want {
		t.Fatalf("expected %q, got %q", want, got)
	}

	withoutRule := RGSelectionReason{Code: "excluded"}
	if got, want := withoutRule.SourceDescription(), "matched policy (excluded)"; got != want {
		t.Fatalf("expected %q, got %q", want, got)
	}
}

func newResourceGroupWithManagedBy(name, managedBy string) *armresources.ResourceGroup {
	return &armresources.ResourceGroup{
		Name:      strPtr(name),
		ManagedBy: strPtr(managedBy),
	}
}

func newResourceGroup(name string, createdAt *time.Time, tags map[string]string, managed bool) *armresources.ResourceGroup {
	resourceGroup := &armresources.ResourceGroup{
		Name: strPtr(name),
	}

	if createdAt != nil {
		if tags == nil {
			tags = map[string]string{}
		}
		tags["createdAt"] = createdAt.UTC().Format(time.RFC3339Nano)
	}

	if len(tags) > 0 {
		resourceGroup.Tags = make(map[string]*string, len(tags))
		for key, value := range tags {
			resourceGroup.Tags[key] = strPtr(value)
		}
	}

	if managed {
		resourceGroup.ManagedBy = strPtr("/subscriptions/example/resourceGroups/example/providers/Microsoft.ManagedIdentity/userAssignedIdentities/example")
	}

	return resourceGroup
}

func strPtr(value string) *string {
	return &value
}

func boolPtr(value bool) *bool {
	return &value
}

func timePtr(value time.Time) *time.Time {
	return &value
}
