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

package setting

import (
	"reflect"
	"testing"

	jsonv2 "github.com/go-json-experiment/json"
)

func TestPolicyScopeIsApplicableSetting(t *testing.T) {
	tests := []struct {
		name           string
		scope          PolicyScope
		setting        *Definition
		wantApplicable bool
	}{
		{
			name:           "DeviceScope/DeviceSetting",
			scope:          DeviceScope,
			setting:        NewDefinition("TestSetting", DeviceSetting, IntegerValue),
			wantApplicable: true,
		},
		{
			name:           "DeviceScope/ProfileSetting",
			scope:          DeviceScope,
			setting:        NewDefinition("TestSetting", ProfileSetting, IntegerValue),
			wantApplicable: false,
		},
		{
			name:           "DeviceScope/UserSetting",
			scope:          DeviceScope,
			setting:        NewDefinition("TestSetting", UserSetting, IntegerValue),
			wantApplicable: false,
		},
		{
			name:           "ProfileScope/DeviceSetting",
			scope:          CurrentProfileScope,
			setting:        NewDefinition("TestSetting", DeviceSetting, IntegerValue),
			wantApplicable: true,
		},
		{
			name:           "ProfileScope/ProfileSetting",
			scope:          CurrentProfileScope,
			setting:        NewDefinition("TestSetting", ProfileSetting, IntegerValue),
			wantApplicable: true,
		},
		{
			name:           "ProfileScope/UserSetting",
			scope:          CurrentProfileScope,
			setting:        NewDefinition("TestSetting", UserSetting, IntegerValue),
			wantApplicable: false,
		},
		{
			name:           "UserScope/DeviceSetting",
			scope:          CurrentUserScope,
			setting:        NewDefinition("TestSetting", DeviceSetting, IntegerValue),
			wantApplicable: true,
		},
		{
			name:           "UserScope/ProfileSetting",
			scope:          CurrentUserScope,
			setting:        NewDefinition("TestSetting", ProfileSetting, IntegerValue),
			wantApplicable: true,
		},
		{
			name:           "UserScope/UserSetting",
			scope:          CurrentUserScope,
			setting:        NewDefinition("TestSetting", UserSetting, IntegerValue),
			wantApplicable: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			gotApplicable := tt.scope.IsApplicableSetting(tt.setting)
			if gotApplicable != tt.wantApplicable {
				t.Fatalf("got %v, want %v", gotApplicable, tt.wantApplicable)
			}
		})
	}
}

func TestPolicyScopeIsConfigurableSetting(t *testing.T) {
	tests := []struct {
		name             string
		scope            PolicyScope
		setting          *Definition
		wantConfigurable bool
	}{
		{
			name:             "DeviceScope/DeviceSetting",
			scope:            DeviceScope,
			setting:          NewDefinition("TestSetting", DeviceSetting, IntegerValue),
			wantConfigurable: true,
		},
		{
			name:             "DeviceScope/ProfileSetting",
			scope:            DeviceScope,
			setting:          NewDefinition("TestSetting", ProfileSetting, IntegerValue),
			wantConfigurable: true,
		},
		{
			name:             "DeviceScope/UserSetting",
			scope:            DeviceScope,
			setting:          NewDefinition("TestSetting", UserSetting, IntegerValue),
			wantConfigurable: true,
		},
		{
			name:             "ProfileScope/DeviceSetting",
			scope:            CurrentProfileScope,
			setting:          NewDefinition("TestSetting", DeviceSetting, IntegerValue),
			wantConfigurable: false,
		},
		{
			name:             "ProfileScope/ProfileSetting",
			scope:            CurrentProfileScope,
			setting:          NewDefinition("TestSetting", ProfileSetting, IntegerValue),
			wantConfigurable: true,
		},
		{
			name:             "ProfileScope/UserSetting",
			scope:            CurrentProfileScope,
			setting:          NewDefinition("TestSetting", UserSetting, IntegerValue),
			wantConfigurable: true,
		},
		{
			name:             "UserScope/DeviceSetting",
			scope:            CurrentUserScope,
			setting:          NewDefinition("TestSetting", DeviceSetting, IntegerValue),
			wantConfigurable: false,
		},
		{
			name:             "UserScope/ProfileSetting",
			scope:            CurrentUserScope,
			setting:          NewDefinition("TestSetting", ProfileSetting, IntegerValue),
			wantConfigurable: false,
		},
		{
			name:             "UserScope/UserSetting",
			scope:            CurrentUserScope,
			setting:          NewDefinition("TestSetting", UserSetting, IntegerValue),
			wantConfigurable: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			gotConfigurable := tt.scope.IsConfigurableSetting(tt.setting)
			if gotConfigurable != tt.wantConfigurable {
				t.Fatalf("got %v, want %v", gotConfigurable, tt.wantConfigurable)
			}
		})
	}
}

func TestPolicyScopeContains(t *testing.T) {
	tests := []struct {
		name                   string
		scopeA                 PolicyScope
		scopeB                 PolicyScope
		wantAContainsB         bool
		wantAStrictlyContainsB bool
	}{
		{
			name:                   "DeviceScope/DeviceScope",
			scopeA:                 DeviceScope,
			scopeB:                 DeviceScope,
			wantAContainsB:         true,
			wantAStrictlyContainsB: false,
		},
		{
			name:                   "DeviceScope/CurrentProfileScope",
			scopeA:                 DeviceScope,
			scopeB:                 CurrentProfileScope,
			wantAContainsB:         true,
			wantAStrictlyContainsB: true,
		},
		{
			name:                   "DeviceScope/UserScope",
			scopeA:                 DeviceScope,
			scopeB:                 CurrentUserScope,
			wantAContainsB:         true,
			wantAStrictlyContainsB: true,
		},
		{
			name:                   "ProfileScope/DeviceScope",
			scopeA:                 CurrentProfileScope,
			scopeB:                 DeviceScope,
			wantAContainsB:         false,
			wantAStrictlyContainsB: false,
		},
		{
			name:                   "ProfileScope/ProfileScope",
			scopeA:                 CurrentProfileScope,
			scopeB:                 CurrentProfileScope,
			wantAContainsB:         true,
			wantAStrictlyContainsB: false,
		},
		{
			name:                   "ProfileScope/UserScope",
			scopeA:                 CurrentProfileScope,
			scopeB:                 CurrentUserScope,
			wantAContainsB:         true,
			wantAStrictlyContainsB: true,
		},
		{
			name:                   "UserScope/DeviceScope",
			scopeA:                 CurrentUserScope,
			scopeB:                 DeviceScope,
			wantAContainsB:         false,
			wantAStrictlyContainsB: false,
		},
		{
			name:                   "UserScope/ProfileScope",
			scopeA:                 CurrentUserScope,
			scopeB:                 CurrentProfileScope,
			wantAContainsB:         false,
			wantAStrictlyContainsB: false,
		},
		{
			name:                   "UserScope/UserScope",
			scopeA:                 CurrentUserScope,
			scopeB:                 CurrentUserScope,
			wantAContainsB:         true,
			wantAStrictlyContainsB: false,
		},
		{
			name:                   "UserScope(1234)/UserScope(1234)",
			scopeA:                 UserScopeOf("1234"),
			scopeB:                 UserScopeOf("1234"),
			wantAContainsB:         true,
			wantAStrictlyContainsB: false,
		},
		{
			name:                   "UserScope(1234)/UserScope(5678)",
			scopeA:                 UserScopeOf("1234"),
			scopeB:                 UserScopeOf("5678"),
			wantAContainsB:         false,
			wantAStrictlyContainsB: false,
		},
		{
			name:                   "ProfileScope(A)/UserScope(A/1234)",
			scopeA:                 PolicyScope{kind: ProfileSetting, profileID: "A"},
			scopeB:                 PolicyScope{kind: UserSetting, userID: "1234", profileID: "A"},
			wantAContainsB:         true,
			wantAStrictlyContainsB: true,
		},
		{
			name:                   "ProfileScope(A)/UserScope(B/1234)",
			scopeA:                 PolicyScope{kind: ProfileSetting, profileID: "A"},
			scopeB:                 PolicyScope{kind: UserSetting, userID: "1234", profileID: "B"},
			wantAContainsB:         false,
			wantAStrictlyContainsB: false,
		},
		{
			name:                   "UserScope(1234)/UserScope(A/1234)",
			scopeA:                 PolicyScope{kind: UserSetting, userID: "1234"},
			scopeB:                 PolicyScope{kind: UserSetting, userID: "1234", profileID: "A"},
			wantAContainsB:         true,
			wantAStrictlyContainsB: true,
		},
		{
			name:                   "UserScope(1234)/UserScope(A/5678)",
			scopeA:                 PolicyScope{kind: UserSetting, userID: "1234"},
			scopeB:                 PolicyScope{kind: UserSetting, userID: "5678", profileID: "A"},
			wantAContainsB:         false,
			wantAStrictlyContainsB: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			gotContains := tt.scopeA.Contains(tt.scopeB)
			if gotContains != tt.wantAContainsB {
				t.Fatalf("WithinOf: got %v, want %v", gotContains, tt.wantAContainsB)
			}

			gotStrictlyContains := tt.scopeA.StrictlyContains(tt.scopeB)
			if gotStrictlyContains != tt.wantAStrictlyContainsB {
				t.Fatalf("StrictlyWithinOf: got %v, want %v", gotStrictlyContains, tt.wantAStrictlyContainsB)
			}
		})
	}
}

func TestPolicyScopeMarshalUnmarshal(t *testing.T) {
	tests := []struct {
		name      string
		in        any
		wantJSON  string
		wantError bool
	}{
		{
			name: "null-scope",
			in: &struct {
				Scope PolicyScope
			}{},
			wantJSON: `{"Scope":"Device"}`,
		},
		{
			name: "null-scope-omit-zero",
			in: &struct {
				Scope PolicyScope `json:",omitzero"`
			}{},
			wantJSON: `{}`,
		},
		{
			name: "device-scope",
			in: &struct {
				Scope PolicyScope
			}{DeviceScope},
			wantJSON: `{"Scope":"Device"}`,
		},
		{
			name: "current-profile-scope",
			in: &struct {
				Scope PolicyScope
			}{CurrentProfileScope},
			wantJSON: `{"Scope":"Profile"}`,
		},
		{
			name: "current-user-scope",
			in: &struct {
				Scope PolicyScope
			}{CurrentUserScope},
			wantJSON: `{"Scope":"User"}`,
		},
		{
			name: "specific-user-scope",
			in: &struct {
				Scope PolicyScope
			}{UserScopeOf("_")},
			wantJSON: `{"Scope":"User(_)"}`,
		},
		{
			name: "specific-user-scope",
			in: &struct {
				Scope PolicyScope
			}{UserScopeOf("S-1-5-21-3698941153-1525015703-2649197413-1001")},
			wantJSON: `{"Scope":"User(S-1-5-21-3698941153-1525015703-2649197413-1001)"}`,
		},
		{
			name: "specific-profile-scope",
			in: &struct {
				Scope PolicyScope
			}{PolicyScope{kind: ProfileSetting, profileID: "1234"}},
			wantJSON: `{"Scope":"Profile(1234)"}`,
		},
		{
			name: "specific-profile-and-user-scope",
			in: &struct {
				Scope PolicyScope
			}{PolicyScope{
				kind:      UserSetting,
				profileID: "1234",
				userID:    "S-1-5-21-3698941153-1525015703-2649197413-1001",
			}},
			wantJSON: `{"Scope":"Profile(1234)/User(S-1-5-21-3698941153-1525015703-2649197413-1001)"}`,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			gotJSON, err := jsonv2.Marshal(tt.in)
			if err != nil {
				t.Fatalf("Marshal failed: %v", err)
			}
			if string(gotJSON) != tt.wantJSON {
				t.Fatalf("Marshal got %s, want %s", gotJSON, tt.wantJSON)
			}
			wantBack := tt.in
			gotBack := reflect.New(reflect.TypeOf(tt.in).Elem()).Interface()
			err = jsonv2.Unmarshal(gotJSON, gotBack)
			if err != nil {
				t.Fatalf("Unmarshal failed: %v", err)
			}
			if !reflect.DeepEqual(gotBack, wantBack) {
				t.Fatalf("Unmarshal got %+v, want %+v", gotBack, wantBack)
			}
		})
	}
}

func TestPolicyScopeUnmarshalSpecial(t *testing.T) {
	tests := []struct {
		name      string
		json      string
		want      any
		wantError bool
	}{
		{
			name: "empty",
			json: "{}",
			want: &struct {
				Scope PolicyScope
			}{},
		},
		{
			name:      "too-many-scopes",
			json:      `{"Scope":"Device/Profile/User"}`,
			wantError: true,
		},
		{
			name:      "user/profile", // incorrect order
			json:      `{"Scope":"User/Profile"}`,
			wantError: true,
		},
		{
			name: "profile-user-no-params",
			json: `{"Scope":"Profile/User"}`,
			want: &struct {
				Scope PolicyScope
			}{CurrentUserScope},
		},
		{
			name:      "unknown-scope",
			json:      `{"Scope":"Unknown"}`,
			wantError: true,
		},
		{
			name:      "unknown-scope/unknown-scope",
			json:      `{"Scope":"Unknown/Unknown"}`,
			wantError: true,
		},
		{
			name:      "device-scope/unknown-scope",
			json:      `{"Scope":"Device/Unknown"}`,
			wantError: true,
		},
		{
			name:      "unknown-scope/device-scope",
			json:      `{"Scope":"Unknown/Device"}`,
			wantError: true,
		},
		{
			name:      "slash",
			json:      `{"Scope":"/"}`,
			wantError: true,
		},
		{
			name:      "empty",
			json:      `{"Scope": ""`,
			wantError: true,
		},
		{
			name:      "no-closing-bracket",
			json:      `{"Scope": "user(1234"`,
			wantError: true,
		},
		{
			name:      "device-with-id",
			json:      `{"Scope": "device(123)"`,
			wantError: true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := &struct {
				Scope PolicyScope
			}{}
			err := jsonv2.Unmarshal([]byte(tt.json), got)
			if (err != nil) != tt.wantError {
				t.Errorf("Marshal error: got %v, want %v", err, tt.wantError)
			}
			if err != nil {
				return
			}
			if !reflect.DeepEqual(got, tt.want) {
				t.Fatalf("Unmarshal got %+v, want %+v", got, tt.want)
			}
		})
	}

}

func TestExtractScopeAndParams(t *testing.T) {
	tests := []struct {
		name   string
		s      string
		scope  string
		params string
		wantOk bool
	}{
		{
			name:   "empty",
			s:      "",
			wantOk: true,
		},
		{
			name:   "scope-only",
			s:      "device",
			scope:  "device",
			wantOk: true,
		},
		{
			name:   "scope-with-params",
			s:      "user(1234)",
			scope:  "user",
			params: "1234",
			wantOk: true,
		},
		{
			name:   "params-empty-scope",
			s:      "(1234)",
			scope:  "",
			params: "1234",
			wantOk: true,
		},
		{
			name:   "params-with-brackets",
			s:      "test()())))())",
			scope:  "test",
			params: ")())))()",
			wantOk: true,
		},
		{
			name:   "no-closing-bracket",
			s:      "user(1234",
			scope:  "",
			params: "",
			wantOk: false,
		},
		{
			name:   "open-before-close",
			s:      ")user(1234",
			scope:  "",
			params: "",
			wantOk: false,
		},
		{
			name:   "brackets-only",
			s:      ")(",
			scope:  "",
			params: "",
			wantOk: false,
		},
		{
			name:   "closing-bracket",
			s:      ")",
			scope:  "",
			params: "",
			wantOk: false,
		},
		{
			name:   "opening-bracket",
			s:      ")",
			scope:  "",
			params: "",
			wantOk: false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			scope, params, ok := extractScopeAndParams(tt.s)
			if ok != tt.wantOk {
				t.Logf("OK: got %v; want %v", ok, tt.wantOk)
			}
			if scope != tt.scope {
				t.Logf("Scope: got %q; want %q", scope, tt.scope)
			}
			if params != tt.params {
				t.Logf("Params: got %v; want %v", params, tt.params)
			}
		})
	}
}
