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

package metrics

import (
	"errors"
	"testing"

	"tailscale.com/types/lazy"
	"tailscale.com/util/clientmetric"
	"tailscale.com/util/syspolicy/internal"
	"tailscale.com/util/syspolicy/pkey"
	"tailscale.com/util/syspolicy/setting"
)

func TestSettingMetricNames(t *testing.T) {
	tests := []struct {
		name           string
		key            pkey.Key
		scope          setting.Scope
		suffix         string
		typ            clientmetric.Type
		osOverride     string
		wantMetricName string
	}{
		{
			name:           "windows-device-no-suffix",
			key:            "AdminConsole",
			scope:          setting.DeviceSetting,
			suffix:         "",
			typ:            clientmetric.TypeCounter,
			osOverride:     "windows",
			wantMetricName: "windows_syspolicy_AdminConsole",
		},
		{
			name:           "windows-user-no-suffix",
			key:            "AdminConsole",
			scope:          setting.UserSetting,
			suffix:         "",
			typ:            clientmetric.TypeCounter,
			osOverride:     "windows",
			wantMetricName: "windows_syspolicy_AdminConsole_user",
		},
		{
			name:           "windows-profile-no-suffix",
			key:            "AdminConsole",
			scope:          setting.ProfileSetting,
			suffix:         "",
			typ:            clientmetric.TypeCounter,
			osOverride:     "windows",
			wantMetricName: "windows_syspolicy_AdminConsole_profile",
		},
		{
			name:           "windows-profile-err",
			key:            "AdminConsole",
			scope:          setting.ProfileSetting,
			suffix:         "error",
			typ:            clientmetric.TypeCounter,
			osOverride:     "windows",
			wantMetricName: "windows_syspolicy_AdminConsole_profile_error",
		},
		{
			name:           "android-device-no-suffix",
			key:            "AdminConsole",
			scope:          setting.DeviceSetting,
			suffix:         "",
			typ:            clientmetric.TypeCounter,
			osOverride:     "android",
			wantMetricName: "android_syspolicy_AdminConsole",
		},
		{
			name:           "key-path",
			key:            "category/subcategory/setting",
			scope:          setting.DeviceSetting,
			suffix:         "",
			typ:            clientmetric.TypeCounter,
			osOverride:     "fakeos",
			wantMetricName: "fakeos_syspolicy_category_subcategory_setting",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			internal.OSForTesting.SetForTest(t, tt.osOverride, nil)
			metric, ok := newSettingMetric(tt.key, tt.scope, tt.suffix, tt.typ).(*funcMetric)
			if !ok {
				t.Fatal("metric is not a funcMetric")
			}
			if metric.name != tt.wantMetricName {
				t.Errorf("got %q, want %q", metric.name, tt.wantMetricName)
			}
		})
	}
}

func TestScopeMetrics(t *testing.T) {
	tests := []struct {
		name               string
		scope              setting.Scope
		osOverride         string
		wantHasAnyName     string
		wantNumErroredName string
		wantHasAnyType     clientmetric.Type
		wantNumErroredType clientmetric.Type
	}{
		{
			name:               "windows-device",
			scope:              setting.DeviceSetting,
			osOverride:         "windows",
			wantHasAnyName:     "windows_syspolicy_any",
			wantHasAnyType:     clientmetric.TypeGauge,
			wantNumErroredName: "windows_syspolicy_errors",
			wantNumErroredType: clientmetric.TypeCounter,
		},
		{
			name:               "windows-profile",
			scope:              setting.ProfileSetting,
			osOverride:         "windows",
			wantHasAnyName:     "windows_syspolicy_profile_any",
			wantHasAnyType:     clientmetric.TypeGauge,
			wantNumErroredName: "windows_syspolicy_profile_errors",
			wantNumErroredType: clientmetric.TypeCounter,
		},
		{
			name:               "windows-user",
			scope:              setting.UserSetting,
			osOverride:         "windows",
			wantHasAnyName:     "windows_syspolicy_user_any",
			wantHasAnyType:     clientmetric.TypeGauge,
			wantNumErroredName: "windows_syspolicy_user_errors",
			wantNumErroredType: clientmetric.TypeCounter,
		},
		{
			name:               "android-device",
			scope:              setting.DeviceSetting,
			osOverride:         "android",
			wantHasAnyName:     "android_syspolicy_any",
			wantHasAnyType:     clientmetric.TypeGauge,
			wantNumErroredName: "android_syspolicy_errors",
			wantNumErroredType: clientmetric.TypeCounter,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			internal.OSForTesting.SetForTest(t, tt.osOverride, nil)
			metrics := newScopeMetrics(tt.scope)
			hasAny, ok := metrics.hasAny.(*funcMetric)
			if !ok {
				t.Fatal("hasAny is not a funcMetric")
			}
			numErrored, ok := metrics.numErrored.(*funcMetric)
			if !ok {
				t.Fatal("numErrored is not a funcMetric")
			}
			if hasAny.name != tt.wantHasAnyName {
				t.Errorf("hasAny.Name: got %q, want %q", hasAny.name, tt.wantHasAnyName)
			}
			if hasAny.typ != tt.wantHasAnyType {
				t.Errorf("hasAny.Type: got %q, want %q", hasAny.typ, tt.wantHasAnyType)
			}
			if numErrored.name != tt.wantNumErroredName {
				t.Errorf("numErrored.Name: got %q, want %q", numErrored.name, tt.wantNumErroredName)
			}
			if numErrored.typ != tt.wantNumErroredType {
				t.Errorf("hasAny.Type: got %q, want %q", numErrored.typ, tt.wantNumErroredType)
			}
		})
	}
}

type testSettingDetails struct {
	definition *setting.Definition
	origin     *setting.Origin
	value      any
	err        error
}

func TestReportMetrics(t *testing.T) {
	tests := []struct {
		name             string
		osOverride       string
		useMetrics       bool
		settings         []testSettingDetails
		wantMetrics      []TestState
		wantResetMetrics []TestState
	}{
		{
			name:        "none",
			osOverride:  "windows",
			settings:    []testSettingDetails{},
			wantMetrics: []TestState{},
		},
		{
			name:       "single-value",
			osOverride: "windows",
			settings: []testSettingDetails{
				{
					definition: setting.NewDefinition("TestSetting01", setting.DeviceSetting, setting.IntegerValue),
					origin:     setting.NewOrigin(setting.DeviceScope),
					value:      42,
				},
			},
			wantMetrics: []TestState{
				{"windows_syspolicy_any", 1},
				{"windows_syspolicy_TestSetting01", 1},
			},
			wantResetMetrics: []TestState{
				{"windows_syspolicy_any", 0},
				{"windows_syspolicy_TestSetting01", 0},
			},
		},
		{
			name:       "single-error",
			osOverride: "windows",
			settings: []testSettingDetails{
				{
					definition: setting.NewDefinition("TestSetting02", setting.DeviceSetting, setting.IntegerValue),
					origin:     setting.NewOrigin(setting.DeviceScope),
					err:        errors.New("bang!"),
				},
			},
			wantMetrics: []TestState{
				{"windows_syspolicy_errors", 1},
				{"windows_syspolicy_TestSetting02_error", 1},
			},
			wantResetMetrics: []TestState{
				{"windows_syspolicy_errors", 1},
				{"windows_syspolicy_TestSetting02_error", 0},
			},
		},
		{
			name:       "value-and-error",
			osOverride: "windows",
			settings: []testSettingDetails{
				{
					definition: setting.NewDefinition("TestSetting01", setting.DeviceSetting, setting.IntegerValue),
					origin:     setting.NewOrigin(setting.DeviceScope),
					value:      42,
				},
				{
					definition: setting.NewDefinition("TestSetting02", setting.DeviceSetting, setting.IntegerValue),
					origin:     setting.NewOrigin(setting.DeviceScope),
					err:        errors.New("bang!"),
				},
			},

			wantMetrics: []TestState{
				{"windows_syspolicy_any", 1},
				{"windows_syspolicy_errors", 1},
				{"windows_syspolicy_TestSetting01", 1},
				{"windows_syspolicy_TestSetting02_error", 1},
			},
			wantResetMetrics: []TestState{
				{"windows_syspolicy_any", 0},
				{"windows_syspolicy_errors", 1},
				{"windows_syspolicy_TestSetting01", 0},
				{"windows_syspolicy_TestSetting02_error", 0},
			},
		},
		{
			name:       "two-values",
			osOverride: "windows",
			settings: []testSettingDetails{
				{
					definition: setting.NewDefinition("TestSetting01", setting.DeviceSetting, setting.IntegerValue),
					origin:     setting.NewOrigin(setting.DeviceScope),
					value:      42,
				},
				{
					definition: setting.NewDefinition("TestSetting02", setting.DeviceSetting, setting.IntegerValue),
					origin:     setting.NewOrigin(setting.DeviceScope),
					value:      17,
				},
			},
			wantMetrics: []TestState{
				{"windows_syspolicy_any", 1},
				{"windows_syspolicy_TestSetting01", 1},
				{"windows_syspolicy_TestSetting02", 1},
			},
			wantResetMetrics: []TestState{
				{"windows_syspolicy_any", 0},
				{"windows_syspolicy_TestSetting01", 0},
				{"windows_syspolicy_TestSetting02", 0},
			},
		},
		{
			name:       "two-errors",
			osOverride: "windows",
			settings: []testSettingDetails{
				{
					definition: setting.NewDefinition("TestSetting01", setting.DeviceSetting, setting.IntegerValue),
					origin:     setting.NewOrigin(setting.DeviceScope),
					err:        errors.New("bang!"),
				},
				{
					definition: setting.NewDefinition("TestSetting02", setting.DeviceSetting, setting.IntegerValue),
					origin:     setting.NewOrigin(setting.DeviceScope),
					err:        errors.New("bang!"),
				},
			},
			wantMetrics: []TestState{
				{"windows_syspolicy_errors", 2},
				{"windows_syspolicy_TestSetting01_error", 1},
				{"windows_syspolicy_TestSetting02_error", 1},
			},
			wantResetMetrics: []TestState{
				{"windows_syspolicy_errors", 2},
				{"windows_syspolicy_TestSetting01_error", 0},
				{"windows_syspolicy_TestSetting02_error", 0},
			},
		},
		{
			name:       "multi-scope",
			osOverride: "windows",
			settings: []testSettingDetails{
				{
					definition: setting.NewDefinition("TestSetting01", setting.ProfileSetting, setting.IntegerValue),
					origin:     setting.NewOrigin(setting.DeviceScope),
					value:      42,
				},
				{
					definition: setting.NewDefinition("TestSetting02", setting.ProfileSetting, setting.IntegerValue),
					origin:     setting.NewOrigin(setting.CurrentProfileScope),
					err:        errors.New("bang!"),
				},
				{
					definition: setting.NewDefinition("TestSetting03", setting.UserSetting, setting.IntegerValue),
					origin:     setting.NewOrigin(setting.CurrentUserScope),
					value:      17,
				},
			},
			wantMetrics: []TestState{
				{"windows_syspolicy_any", 1},
				{"windows_syspolicy_profile_errors", 1},
				{"windows_syspolicy_user_any", 1},
				{"windows_syspolicy_TestSetting01", 1},
				{"windows_syspolicy_TestSetting02_profile_error", 1},
				{"windows_syspolicy_TestSetting03_user", 1},
			},
			wantResetMetrics: []TestState{
				{"windows_syspolicy_any", 0},
				{"windows_syspolicy_profile_errors", 1},
				{"windows_syspolicy_user_any", 0},
				{"windows_syspolicy_TestSetting01", 0},
				{"windows_syspolicy_TestSetting02_profile_error", 0},
				{"windows_syspolicy_TestSetting03_user", 0},
			},
		},
		{
			name:       "report-metrics-on-android",
			osOverride: "android",
			settings: []testSettingDetails{
				{
					definition: setting.NewDefinition("TestSetting01", setting.DeviceSetting, setting.IntegerValue),
					origin:     setting.NewOrigin(setting.DeviceScope),
					value:      42,
				},
			},
			wantMetrics: []TestState{
				{"android_syspolicy_any", 1},
				{"android_syspolicy_TestSetting01", 1},
			},
			wantResetMetrics: []TestState{
				{"android_syspolicy_any", 0},
				{"android_syspolicy_TestSetting01", 0},
			},
		},
		{
			name:       "do-not-report-metrics-on-macos",
			osOverride: "macos",
			settings: []testSettingDetails{
				{
					definition: setting.NewDefinition("TestSetting01", setting.DeviceSetting, setting.IntegerValue),
					origin:     setting.NewOrigin(setting.DeviceScope),
					value:      42,
				},
			},

			wantMetrics: []TestState{}, // none reported
		},
		{
			name:       "do-not-report-metrics-on-ios",
			osOverride: "ios",
			settings: []testSettingDetails{
				{
					definition: setting.NewDefinition("TestSetting01", setting.DeviceSetting, setting.IntegerValue),
					origin:     setting.NewOrigin(setting.DeviceScope),
					value:      42,
				},
			},

			wantMetrics: []TestState{}, // none reported
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// Reset the lazy value so it'll be re-evaluated with the osOverride.
			lazyReportMetrics = lazy.SyncValue[bool]{}
			t.Cleanup(func() {
				// Also reset it during the cleanup.
				lazyReportMetrics = lazy.SyncValue[bool]{}
			})
			internal.OSForTesting.SetForTest(t, tt.osOverride, nil)

			h := NewTestHandler(t)
			SetHooksForTest(t, h.AddMetric, h.SetMetric)

			for _, s := range tt.settings {
				if s.err != nil {
					ReportError(s.origin, s.definition, s.err)
				} else {
					ReportConfigured(s.origin, s.definition, s.value)
				}
			}
			h.MustEqual(tt.wantMetrics...)

			for _, s := range tt.settings {
				Reset(s.origin)
				ReportNotConfigured(s.origin, s.definition)
			}
			h.MustEqual(tt.wantResetMetrics...)
		})
	}
}
