package runtimeconfiginit

import (
	"fmt"
	"reflect"
	"testing"

	"github.com/aws/amazon-ssm-agent/agent/backoffconfig"
	"github.com/aws/amazon-ssm-agent/agent/log"
	logmocks "github.com/aws/amazon-ssm-agent/agent/mocks/log"
	"github.com/aws/amazon-ssm-agent/common/identity"
	mockIdentity "github.com/aws/amazon-ssm-agent/common/identity/mocks"
	"github.com/aws/amazon-ssm-agent/common/runtimeconfig"
	"github.com/aws/amazon-ssm-agent/common/runtimeconfig/mocks"
	"github.com/cenkalti/backoff/v4"
	"github.com/stretchr/testify/mock"
)

func TestNew(t *testing.T) {
	mockLog := logmocks.NewMockLog()
	mockIdentity := mockIdentity.NewDefaultMockAgentIdentity()
	type args struct {
		log      log.T
		identity identity.IAgentIdentity
	}
	tests := []struct {
		name string
		args args
		want IRuntimeConfigInit
	}{
		{
			"Success",
			args{
				mockLog,
				mockIdentity,
			},
			&runtimeConfigInit{
				mockLog,
				nil,
				mockIdentity,
				runtimeconfig.NewIdentityRuntimeConfigClient()},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := New(tt.args.log, tt.args.identity); !reflect.DeepEqual(got, tt.want) {
				t.Errorf("New() = %v, want %v", got, tt.want)
			}
		})
	}
}

func Test_runtimeConfigInit_getCurrentIdentityRuntimeConfig(t *testing.T) {
	successIdentity := mockIdentity.NewDefaultMockAgentIdentity()
	successConfig := runtimeconfig.IdentityRuntimeConfig{
		InstanceId:   mockIdentity.MockInstanceID,
		IdentityType: mockIdentity.MockIdentityType,
	}

	failureIdentity := &mockIdentity.IAgentIdentity{}
	failureIdentity.On("IdentityType").Return("SomeIdentityType")
	failureIdentity.On("InstanceID").Return("", fmt.Errorf("SomeError"))
	failureConfig := runtimeconfig.IdentityRuntimeConfig{
		IdentityType: "SomeIdentityType",
	}

	type fields struct {
		log                  log.T
		backoffConfig        *backoff.ExponentialBackOff
		agentIdentity        identity.IAgentIdentity
		identityConfigClient runtimeconfig.IIdentityRuntimeConfigClient
	}
	tests := []struct {
		name    string
		fields  fields
		want    runtimeconfig.IdentityRuntimeConfig
		wantErr bool
	}{
		{
			"Success",
			fields{
				nil,
				nil,
				successIdentity,
				nil,
			},
			successConfig,
			false,
		},
		{
			"Failure",
			fields{
				nil,
				nil,
				failureIdentity,
				nil,
			},
			failureConfig,
			true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			r := &runtimeConfigInit{
				log:                  tt.fields.log,
				backoffConfig:        tt.fields.backoffConfig,
				agentIdentity:        tt.fields.agentIdentity,
				identityConfigClient: tt.fields.identityConfigClient,
			}
			got, err := r.getCurrentIdentityRuntimeConfig()
			if (err != nil) != tt.wantErr {
				t.Errorf("getCurrentIdentityRuntimeConfig() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("getCurrentIdentityRuntimeConfig() got = %v, want %v", got, tt.want)
			}
		})
	}
}

func Test_runtimeConfigInit_initIdentityRuntimeConfig(t *testing.T) {
	var emptyConfig runtimeconfig.IdentityRuntimeConfig
	successIdentity := mockIdentity.NewDefaultMockAgentIdentity()
	currentConfig := runtimeconfig.IdentityRuntimeConfig{
		InstanceId:   mockIdentity.MockInstanceID,
		IdentityType: mockIdentity.MockIdentityType,
	}
	otherConfig := runtimeconfig.IdentityRuntimeConfig{
		InstanceId:   "OtherInstanceID",
		IdentityType: "OtherIdentityType",
	}

	failureIdentity := &mockIdentity.IAgentIdentity{}
	failureIdentity.On("IdentityType").Return("SomeIdentityType")
	failureIdentity.On("InstanceID").Return("", fmt.Errorf("SomeError"))

	icc_equalConfig := &mocks.IIdentityRuntimeConfigClient{}
	icc_equalConfig.On("ConfigExists").Return(true, nil)
	icc_equalConfig.On("GetConfig").Return(currentConfig, nil)

	icc_notEqualConfig := &mocks.IIdentityRuntimeConfigClient{}
	icc_notEqualConfig.On("SaveConfig", currentConfig).Return(nil)
	icc_notEqualConfig.On("ConfigExists").Return(true, nil)
	icc_notEqualConfig.On("GetConfig").Return(otherConfig, nil)

	icc_configNotExist := &mocks.IIdentityRuntimeConfigClient{}
	icc_configNotExist.On("SaveConfig", currentConfig).Return(nil)
	icc_configNotExist.On("ConfigExists").Return(false, nil)

	icc_configExistErr := &mocks.IIdentityRuntimeConfigClient{}
	icc_configExistErr.On("SaveConfig", currentConfig).Return(nil)
	icc_configExistErr.On("ConfigExists").Return(false, fmt.Errorf("SomeError"))

	icc_errGetConfig := &mocks.IIdentityRuntimeConfigClient{}
	icc_errGetConfig.On("SaveConfig", currentConfig).Return(nil)
	icc_errGetConfig.On("ConfigExists").Return(true, nil)
	icc_errGetConfig.On("GetConfig").Return(emptyConfig, fmt.Errorf("SomeError"))

	icc_saveConfigErr := &mocks.IIdentityRuntimeConfigClient{}
	icc_saveConfigErr.On("SaveConfig", currentConfig).Return(fmt.Errorf("SomeError"))
	icc_saveConfigErr.On("ConfigExists").Return(true, nil)
	icc_saveConfigErr.On("GetConfig").Return(otherConfig, nil)

	backoffRetry = func(fun backoff.Operation, _ backoff.BackOff) error {
		return fun()
	}

	type fields struct {
		log                  log.T
		backoffConfig        *backoff.ExponentialBackOff
		agentIdentity        identity.IAgentIdentity
		identityConfigClient runtimeconfig.IIdentityRuntimeConfigClient
	}
	tests := []struct {
		name    string
		fields  fields
		wantErr bool
	}{
		{
			"FailedGetCurrentRuntimeConfig",
			fields{
				nil,
				nil,
				failureIdentity,
				nil,
			},
			true,
		},
		{
			"Success_ConfigNotEqual",
			fields{
				logmocks.NewMockLog(),
				nil,
				successIdentity,
				icc_notEqualConfig,
			},
			false,
		},

		{
			"Success_ConfigEqual",
			fields{
				logmocks.NewMockLog(),
				nil,
				successIdentity,
				icc_equalConfig,
			},
			false,
		},
		{
			"Success_ConfigNotExistEqual",
			fields{
				logmocks.NewMockLog(),
				nil,
				successIdentity,
				icc_configNotExist,
			},
			false,
		},
		{
			"Success_ConfigExistErr",
			fields{
				logmocks.NewMockLog(),
				nil,
				successIdentity,
				icc_configExistErr,
			},
			false,
		},
		{
			"Success_GetConfigErr",
			fields{
				logmocks.NewMockLog(),
				nil,
				successIdentity,
				icc_errGetConfig,
			},
			false,
		},
		{
			"Failed_SaveConfigErr",
			fields{
				logmocks.NewMockLog(),
				nil,
				successIdentity,
				icc_saveConfigErr,
			},
			true,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			r := &runtimeConfigInit{
				log:                  tt.fields.log,
				backoffConfig:        tt.fields.backoffConfig,
				agentIdentity:        tt.fields.agentIdentity,
				identityConfigClient: tt.fields.identityConfigClient,
			}
			if err := r.initIdentityRuntimeConfig(); (err != nil) != tt.wantErr {
				t.Errorf("initIdentityRuntimeConfig() error = %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}

func Test_runtimeConfigInit_saveIdentityConfigWithRetry(t *testing.T) {
	icc := &mocks.IIdentityRuntimeConfigClient{}
	mockLog := logmocks.NewMockLog()

	successConfig := runtimeconfig.IdentityRuntimeConfig{
		InstanceId:   "InstanceId",
		IdentityType: "IdentityType",
	}
	failureConfig := runtimeconfig.IdentityRuntimeConfig{}

	icc.On("SaveConfig", failureConfig).Return(fmt.Errorf("SomeError"))
	icc.On("SaveConfig", successConfig).Return(nil)

	mockLog.On("Debugf", mock.Anything).Return(nil)
	mockLog.On("Warnf", "Failed to save runtime config: %v", mock.Anything).Return(nil)
	backoffRetry = func(fun backoff.Operation, _ backoff.BackOff) error {
		return fun()
	}

	type fields struct {
		log                  log.T
		backoffConfig        *backoff.ExponentialBackOff
		agentIdentity        identity.IAgentIdentity
		identityConfigClient runtimeconfig.IIdentityRuntimeConfigClient
	}
	type args struct {
		currentConfig runtimeconfig.IdentityRuntimeConfig
	}
	tests := []struct {
		name    string
		fields  fields
		args    args
		wantErr bool
	}{
		{
			"Failed",
			fields{
				logmocks.NewMockLog(),
				nil,
				nil,
				icc,
			},
			args{
				failureConfig,
			},
			true,
		},
		{
			"Success",
			fields{
				mockLog,
				nil,
				nil,
				icc,
			},
			args{
				successConfig,
			},
			false,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			r := &runtimeConfigInit{
				log:                  tt.fields.log,
				backoffConfig:        tt.fields.backoffConfig,
				agentIdentity:        tt.fields.agentIdentity,
				identityConfigClient: tt.fields.identityConfigClient,
			}
			if err := r.saveIdentityConfigWithRetry(tt.args.currentConfig); (err != nil) != tt.wantErr {
				t.Errorf("saveIdentityConfigWithRetry() error = %v, wantErr %v", err, tt.wantErr)
			}
		})
	}
}

func Test_runtimeConfigInit_Init(t *testing.T) {
	// Store original function
	originalBackoff := backoffconfig.GetDefaultExponentialBackoff
	// Restore after test
	defer func() {
		getDefaultExponentialBackoff = originalBackoff
	}()

	mockLog := logmocks.NewMockLog()
	mockIdentity := mockIdentity.NewDefaultMockAgentIdentity()
	mockConfigClient := &mocks.IIdentityRuntimeConfigClient{}

	mockConfigClient.On("ConfigExists").Return(true, nil)
	mockConfigClient.On("GetConfig").Return(runtimeconfig.IdentityRuntimeConfig{}, nil)
	mockConfigClient.On("SaveConfig", mock.Anything).Return(nil)

	type fields struct {
		log                  log.T
		backoffConfig        *backoff.ExponentialBackOff
		agentIdentity        identity.IAgentIdentity
		identityConfigClient runtimeconfig.IIdentityRuntimeConfigClient
	}

	tests := []struct {
		name    string
		fields  fields
		wantErr bool
	}{
		{
			name: "successful_initialization",
			fields: fields{
				log:                  mockLog,
				agentIdentity:        mockIdentity,
				identityConfigClient: mockConfigClient,
			},
			wantErr: false,
		},
		{
			name: "backoff_config_error",
			fields: fields{
				log:                  mockLog,
				agentIdentity:        mockIdentity,
				identityConfigClient: mockConfigClient,
			},
			wantErr: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if tt.name == "backoff_config_error" {
				getDefaultExponentialBackoff = func() (*backoff.ExponentialBackOff, error) {
					return nil, fmt.Errorf("mock backoff error")
				}
			} else {
				getDefaultExponentialBackoff = originalBackoff
			}

			r := &runtimeConfigInit{
				log:                  tt.fields.log,
				backoffConfig:        tt.fields.backoffConfig,
				agentIdentity:        tt.fields.agentIdentity,
				identityConfigClient: tt.fields.identityConfigClient,
			}

			err := r.Init()
			if (err != nil) != tt.wantErr {
				t.Errorf("Init() error = %v, wantErr %v", err, tt.wantErr)
			}

			if !tt.wantErr {
				// Verify backoffConfig was set
				if r.backoffConfig == nil {
					t.Error("backoffConfig should not be nil on success")
				}
			}

			mockConfigClient.AssertExpectations(t)
		})
	}
}
