// Copyright IBM Corp. 2016, 2025
// SPDX-License-Identifier: MPL-2.0

package automatedrotationutil

import (
	"reflect"
	"strings"
	"testing"
	"time"

	"github.com/hashicorp/vault/sdk/framework"
	"github.com/stretchr/testify/assert"
)

var schemaMap = map[string]*framework.FieldSchema{
	"rotation_schedule": {
		Type:        framework.TypeString,
		Description: "CRON-style string that will define the schedule on which rotations should occur. Mutually exclusive with rotation_period",
		DisplayAttrs: &framework.DisplayAttributes{
			Group: "default",
		},
	},
	"rotation_window": {
		Type:        framework.TypeDurationSecond,
		Description: "Specifies the amount of time in which the rotation is allowed to occur starting from a given rotation_schedule",
		DisplayAttrs: &framework.DisplayAttributes{
			Group: "default",
		},
	},
	"rotation_period": {
		Type:        framework.TypeDurationSecond,
		Description: "TTL for automatic credential rotation of the given username. Mutually exclusive with rotation_schedule",
		DisplayAttrs: &framework.DisplayAttributes{
			Group: "default",
		},
	},
	"disable_automated_rotation": {
		Type:        framework.TypeBool,
		Description: "If set to true, will deregister all registered rotation jobs from the RotationManager for the plugin.",
		DisplayAttrs: &framework.DisplayAttributes{
			Group: "default",
		},
	},
}

func TestParseAutomatedRotationFields(t *testing.T) {
	tests := []struct {
		name           string
		data           *framework.FieldData
		expectedParams *AutomatedRotationParams
		initialParams  *AutomatedRotationParams
		expectedError  string
	}{
		{
			name: "basic",
			data: &framework.FieldData{
				Raw: map[string]interface{}{
					"rotation_schedule": "*/15 * * * *",
					"rotation_window":   60,
				},
				Schema: schemaMap,
			},
			expectedParams: &AutomatedRotationParams{
				RotationSchedule:         "*/15 * * * *",
				RotationWindow:           60 * time.Second,
				RotationPeriod:           0,
				DisableAutomatedRotation: false,
			},
		},
		{
			name: "mutually-exclusive",
			data: &framework.FieldData{
				Raw: map[string]interface{}{
					"rotation_schedule": "*/15 * * * *",
					"rotation_period":   15,
					"rotation_window":   60,
				},
				Schema: schemaMap,
			},
			expectedError: ErrRotationMutuallyExclusiveFields.Error(),
		},
		{
			name: "bad-schedule",
			data: &framework.FieldData{
				Raw: map[string]interface{}{
					"rotation_schedule": "random string",
				},
				Schema: schemaMap,
			},
			expectedError: "failed to parse provided rotation_schedule",
		},
		{
			name: "incompatible-fields",
			data: &framework.FieldData{
				Raw: map[string]interface{}{
					"rotation_period": 15,
					"rotation_window": 60,
				},
				Schema: schemaMap,
			},
			expectedError: "rotation_window does not apply to period",
		},
		{
			name: "window-without-schedule",
			data: &framework.FieldData{
				Raw: map[string]interface{}{
					"rotation_window": 60,
				},
				Schema: schemaMap,
			},
			expectedError: "cannot use rotation_window without rotation_schedule",
		},
		{
			name: "rotation-period-duration-seconds",
			data: &framework.FieldData{
				Raw: map[string]interface{}{
					"rotation_period": "2m",
				},
				Schema: schemaMap,
			},
			expectedParams: &AutomatedRotationParams{
				RotationSchedule:         "",
				RotationWindow:           0,
				RotationPeriod:           120 * time.Second,
				DisableAutomatedRotation: false,
			},
		},
		{
			name: "rotation-window-duration-seconds",
			data: &framework.FieldData{
				Raw: map[string]interface{}{
					"rotation_window":   "12h",
					"rotation_schedule": "* */2 * * *",
				},
				Schema: schemaMap,
			},
			expectedParams: &AutomatedRotationParams{
				RotationSchedule:         "* */2 * * *",
				RotationWindow:           12 * time.Hour,
				RotationPeriod:           0,
				DisableAutomatedRotation: false,
			},
		},
		{
			name: "period-and-window-ok",
			data: &framework.FieldData{
				Raw: map[string]interface{}{
					"rotation_window": 0,
					"rotation_period": 10,
				},
				Schema: schemaMap,
			},
			expectedParams: &AutomatedRotationParams{
				RotationSchedule:         "",
				RotationWindow:           0,
				RotationPeriod:           10 * time.Second,
				DisableAutomatedRotation: false,
			},
		},
		{
			name: "period-and-window-ok-strings",
			data: &framework.FieldData{
				Raw: map[string]interface{}{
					"rotation_schedule": "* */2 * * *",
					"rotation_window":   "5h",
					"rotation_period":   "",
				},
				Schema: schemaMap,
			},
			expectedParams: &AutomatedRotationParams{
				RotationSchedule:         "* */2 * * *",
				RotationWindow:           5 * time.Hour,
				RotationPeriod:           0,
				DisableAutomatedRotation: false,
			},
		},
		{
			name: "period-and-schedule-ok",
			data: &framework.FieldData{
				Raw: map[string]interface{}{
					"rotation_schedule": "",
					"rotation_window":   "",
					"rotation_period":   "2m",
				},
				Schema: schemaMap,
			},
			expectedParams: &AutomatedRotationParams{
				RotationSchedule:         "",
				RotationWindow:           0,
				RotationPeriod:           2 * time.Minute,
				DisableAutomatedRotation: false,
			},
		},
		{
			name: "zero-out-schedule-and-window-set-period",
			data: &framework.FieldData{
				Raw: map[string]interface{}{
					"rotation_schedule": "",
					"rotation_window":   "",
					"rotation_period":   "2m",
				},
				Schema: schemaMap,
			},
			expectedParams: &AutomatedRotationParams{
				RotationSchedule:         "",
				RotationWindow:           0,
				RotationPeriod:           2 * time.Minute,
				DisableAutomatedRotation: false,
			},
			initialParams: &AutomatedRotationParams{
				RotationSchedule:         "*/1 * * * *",
				RotationWindow:           30 * time.Second,
				RotationPeriod:           0,
				DisableAutomatedRotation: false,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			p := &AutomatedRotationParams{}
			if tt.initialParams != nil {
				p = tt.initialParams
			}
			err := p.ParseAutomatedRotationFields(tt.data)
			if err != nil {
				if tt.expectedError == "" {
					t.Errorf("expected no error but received an error: %s", err)
				}
				if !strings.Contains(err.Error(), tt.expectedError) {
					t.Errorf("ParseAutomatedRotationFields() error = %v, expected %s", err, tt.expectedError)
				}
			}

			if err == nil && !reflect.DeepEqual(tt.expectedParams, p) {
				t.Errorf("ParseAutomatedRotationFields() error comparing params; got %v, expected %v", p, tt.expectedParams)
			}
		})
	}
}

func TestPopulateAutomatedRotationData(t *testing.T) {
	tests := []struct {
		name        string
		inputParams *AutomatedRotationParams
		expected    map[string]interface{}
	}{
		{
			name: "basic",
			expected: map[string]interface{}{
				"rotation_schedule":          "*/15 * * * *",
				"rotation_window":            time.Duration(60).Seconds(),
				"rotation_period":            time.Duration(0).Seconds(),
				"disable_automated_rotation": false,
			},
			inputParams: &AutomatedRotationParams{
				RotationSchedule:         "*/15 * * * *",
				RotationWindow:           60,
				RotationPeriod:           0,
				DisableAutomatedRotation: false,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			m := make(map[string]interface{})

			tt.inputParams.PopulateAutomatedRotationData(m)

			if !reflect.DeepEqual(m, tt.expected) {
				t.Errorf("PopulateAutomatedRotationData() error comparing values; got %v, expected %v", m, tt.expected)
			}
		})
	}
}

func TestShouldRegisterRotationJob(t *testing.T) {
	tests := []struct {
		name        string
		inputParams *AutomatedRotationParams
		expected    bool
	}{
		{
			name:     "false",
			expected: false,
			inputParams: &AutomatedRotationParams{
				RotationSchedule:         "",
				RotationWindow:           60,
				RotationPeriod:           0,
				DisableAutomatedRotation: false,
			},
		},
		{
			name:     "true-schedule",
			expected: true,
			inputParams: &AutomatedRotationParams{
				RotationSchedule:         "*/15 * * * *",
				RotationWindow:           0,
				RotationPeriod:           0,
				DisableAutomatedRotation: false,
			},
		},
		{
			name:     "true-period",
			expected: true,
			inputParams: &AutomatedRotationParams{
				RotationSchedule:         "",
				RotationWindow:           0,
				RotationPeriod:           5,
				DisableAutomatedRotation: false,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			out := tt.inputParams.ShouldRegisterRotationJob()
			if out != tt.expected {
				t.Errorf("ShouldRegisterRotationJob() output = %v, expected: %v", out, tt.expected)
			}
		})
	}
}

func TestShouldDeregisterRotationJob(t *testing.T) {
	tests := []struct {
		name        string
		inputParams *AutomatedRotationParams
		expected    bool
	}{
		{
			name:     "zero out schedule and period",
			expected: true,
			inputParams: &AutomatedRotationParams{
				RotationSchedule:         "",
				RotationWindow:           60,
				RotationPeriod:           0,
				DisableAutomatedRotation: false,
			},
		},
		{
			name:     "false-schedule set",
			expected: false,
			inputParams: &AutomatedRotationParams{
				RotationSchedule:         "*/15 * * * *",
				RotationWindow:           0,
				RotationPeriod:           0,
				DisableAutomatedRotation: false,
			},
		},
		{
			name:     "false-period set",
			expected: false,
			inputParams: &AutomatedRotationParams{
				RotationSchedule:         "",
				RotationWindow:           0,
				RotationPeriod:           5,
				DisableAutomatedRotation: false,
			},
		},
		{
			name:     "true-disable set",
			expected: true,
			inputParams: &AutomatedRotationParams{
				RotationSchedule:         "*/15 * * * *",
				RotationWindow:           0,
				RotationPeriod:           0,
				DisableAutomatedRotation: true,
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			out := tt.inputParams.ShouldDeregisterRotationJob()
			if out != tt.expected {
				t.Errorf("ShouldRegisterRotationJob() output = %v, expected: %v", out, tt.expected)
			}
		})
	}
}

func TestAddAutomatedRotationFieldsWithGroup(t *testing.T) {
	expectedSchemaMap := make(map[string]*framework.FieldSchema)
	for k, v := range schemaMap {
		newField := *v
		newDisplayAttrs := *v.DisplayAttrs
		newDisplayAttrs.Group = "Automated Rotation"
		newField.DisplayAttrs = &newDisplayAttrs
		expectedSchemaMap[k] = &newField
	}
	testcases := []struct {
		name  string
		group string
		input map[string]*framework.FieldSchema
		want  map[string]*framework.FieldSchema
	}{
		{
			name:  "basic",
			input: map[string]*framework.FieldSchema{},
			group: "Automated Rotation",
			want:  expectedSchemaMap,
		},
	}

	for _, tt := range testcases {
		t.Run(tt.name, func(t *testing.T) {
			got := tt.input
			AddAutomatedRotationFieldsWithGroup(got, tt.group)
			assert.Equal(t, tt.want, got)
		})
	}
}

func TestAddAutomatedRotationFields(t *testing.T) {
	testcases := []struct {
		name  string
		group string
		input map[string]*framework.FieldSchema
		want  map[string]*framework.FieldSchema
	}{
		{
			name:  "basic",
			input: map[string]*framework.FieldSchema{},
			want:  schemaMap,
		},
	}

	for _, tt := range testcases {
		t.Run(tt.name, func(t *testing.T) {
			got := tt.input
			AddAutomatedRotationFields(got)
			assert.Equal(t, tt.want, got)
		})
	}
}
