/*
Copyright © The ESO Authors

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

    https://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 volcengine

import (
	"context"
	"errors"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/volcengine/volcengine-go-sdk/service/kms"
	"github.com/volcengine/volcengine-go-sdk/volcengine/request"
	corev1 "k8s.io/api/core/v1"
	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"

	esapi "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
)

type fakePushSecretData struct {
	Metadata  *apiextensionsv1.JSON
	SecretKey string
	RemoteKey string
	Property  string
}

func (f fakePushSecretData) GetMetadata() *apiextensionsv1.JSON {
	return f.Metadata
}

func (f fakePushSecretData) GetSecretKey() string {
	return f.SecretKey
}

func (f fakePushSecretData) GetRemoteKey() string {
	return f.RemoteKey
}

func (f fakePushSecretData) GetProperty() string {
	return f.Property
}

type fakePushScretRemoteRef struct {
	RemoteKey string
	Property  string
}

func (f fakePushScretRemoteRef) GetRemoteKey() string {
	return f.RemoteKey
}

func (f fakePushScretRemoteRef) GetProperty() string {
	return f.Property
}

// MockKMSClient is a mock of KMSAPI interface.
type MockKMSClient struct {
	kms.KMSAPI
	DescribeRegionsFunc           func(*kms.DescribeRegionsInput) (*kms.DescribeRegionsOutput, error)
	DescribeSecretWithContextFunc func(context.Context, *kms.DescribeSecretInput, ...request.Option) (*kms.DescribeSecretOutput, error)
	GetSecretValueWithContextFunc func(context.Context, *kms.GetSecretValueInput, ...request.Option) (*kms.GetSecretValueOutput, error)
}

// DescribeRegions mocks the DescribeRegions method.
func (m *MockKMSClient) DescribeRegions(input *kms.DescribeRegionsInput) (*kms.DescribeRegionsOutput, error) {
	if m.DescribeRegionsFunc != nil {
		return m.DescribeRegionsFunc(input)
	}
	return nil, errors.New("DescribeRegions is not implemented")
}

// DescribeSecretWithContext mocks the DescribeSecretWithContext method.
func (m *MockKMSClient) DescribeSecretWithContext(ctx context.Context, input *kms.DescribeSecretInput, opts ...request.Option) (*kms.DescribeSecretOutput, error) {
	if m.DescribeSecretWithContextFunc != nil {
		return m.DescribeSecretWithContextFunc(ctx, input, opts...)
	}
	return nil, errors.New("DescribeSecretWithContext is not implemented")
}

// GetSecretValueWithContext mocks the GetSecretValueWithContext method.
func (m *MockKMSClient) GetSecretValueWithContext(ctx context.Context, input *kms.GetSecretValueInput, opts ...request.Option) (*kms.GetSecretValueOutput, error) {
	if m.GetSecretValueWithContextFunc != nil {
		return m.GetSecretValueWithContextFunc(ctx, input, opts...)
	}
	return nil, errors.New("GetSecretValueWithContext is not implemented")
}

func TestNew_should_return_a_new_client(t *testing.T) {
	mockKMS := &MockKMSClient{}
	client := NewClient(mockKMS)
	assert.NotNil(t, client)
	assert.Equal(t, mockKMS, client.kms)
}

func TestClient_PushSecret_should_return_not_implemented_error(t *testing.T) {
	client := &Client{}
	err := client.PushSecret(context.Background(), &corev1.Secret{}, &fakePushSecretData{})
	assert.Error(t, err)
	assert.Equal(t, notImplemented, err.Error())
}

func TestClient_DeleteSecret_should_return_not_implemented_error(t *testing.T) {
	client := &Client{}
	err := client.DeleteSecret(context.Background(), &fakePushSecretData{})
	assert.Error(t, err)
	assert.Equal(t, notImplemented, err.Error())
}

func TestClient_GetAllSecrets_should_return_not_implemented_error(t *testing.T) {
	client := &Client{}
	_, err := client.GetAllSecrets(context.Background(), esapi.ExternalSecretFind{})
	assert.Error(t, err)
	assert.Equal(t, notImplemented, err.Error())
}

func TestClient_Close_should_return_nil(t *testing.T) {
	client := &Client{}
	err := client.Close(context.Background())
	assert.NoError(t, err)
}

func TestClient_Validate_should_return_ready_when_kms_client_is_initialized(t *testing.T) {
	mockKMS := &MockKMSClient{
		DescribeRegionsFunc: func(*kms.DescribeRegionsInput) (*kms.DescribeRegionsOutput, error) {
			return &kms.DescribeRegionsOutput{}, nil
		},
	}
	client := NewClient(mockKMS)
	result, err := client.Validate()
	assert.NoError(t, err)
	assert.Equal(t, esapi.ValidationResultReady, result)
}

func TestClient_Validate_should_return_error_when_kms_client_is_not_initialized(t *testing.T) {
	client := NewClient(nil)
	result, err := client.Validate()
	assert.Error(t, err)
	assert.Equal(t, "kms client is not initialized", err.Error())
	assert.Equal(t, esapi.ValidationResultError, result)
}

func TestClient_GetSecret_should_return_secret_value_when_property_is_empty(t *testing.T) {
	secretValue := "my-secret-value"
	mockKMS := &MockKMSClient{
		GetSecretValueWithContextFunc: func(ctx context.Context, input *kms.GetSecretValueInput, opts ...request.Option) (*kms.GetSecretValueOutput, error) {
			return &kms.GetSecretValueOutput{
				SecretValue: &secretValue,
			}, nil
		},
	}
	client := NewClient(mockKMS)
	value, err := client.GetSecret(context.Background(), esapi.ExternalSecretDataRemoteRef{
		Key: "my-secret",
	})
	assert.NoError(t, err)
	assert.Equal(t, []byte(secretValue), value)
}

func TestClient_GetSecret_should_return_property_value_when_secret_is_json_and_property_exists(t *testing.T) {
	secretValue := `{"user":"admin","pass":"1234"}`
	mockKMS := &MockKMSClient{
		GetSecretValueWithContextFunc: func(ctx context.Context, input *kms.GetSecretValueInput, opts ...request.Option) (*kms.GetSecretValueOutput, error) {
			return &kms.GetSecretValueOutput{
				SecretValue: &secretValue,
			}, nil
		},
	}
	client := NewClient(mockKMS)
	value, err := client.GetSecret(context.Background(), esapi.ExternalSecretDataRemoteRef{
		Key:      "my-secret",
		Property: "pass",
	})
	assert.NoError(t, err)
	assert.Equal(t, []byte("1234"), value)
}

func TestClient_GetSecret_should_return_raw_json_value_when_property_is_json_object(t *testing.T) {
	secretValue := `{"config":{"foo":"bar"},"pass":"1234"}`
	mockKMS := &MockKMSClient{
		GetSecretValueWithContextFunc: func(ctx context.Context, input *kms.GetSecretValueInput, opts ...request.Option) (*kms.GetSecretValueOutput, error) {
			return &kms.GetSecretValueOutput{
				SecretValue: &secretValue,
			}, nil
		},
	}
	client := NewClient(mockKMS)
	value, err := client.GetSecret(context.Background(), esapi.ExternalSecretDataRemoteRef{
		Key:      "my-secret",
		Property: "config",
	})
	assert.NoError(t, err)
	assert.Equal(t, []byte(`{"foo":"bar"}`), value)
}

func TestClient_GetSecret_should_return_error_when_property_does_not_exist(t *testing.T) {
	secretValue := `{"user":"admin","pass":"1234"}`
	mockKMS := &MockKMSClient{
		GetSecretValueWithContextFunc: func(ctx context.Context, input *kms.GetSecretValueInput, opts ...request.Option) (*kms.GetSecretValueOutput, error) {
			return &kms.GetSecretValueOutput{
				SecretValue: &secretValue,
			}, nil
		},
	}
	client := NewClient(mockKMS)
	_, err := client.GetSecret(context.Background(), esapi.ExternalSecretDataRemoteRef{
		Key:      "my-secret",
		Property: "non-existent",
	})
	assert.Error(t, err)
	assert.Equal(t, `property "non-existent" not found in secret`, err.Error())
}

func TestClient_GetSecret_should_return_error_when_secret_is_not_valid_json(t *testing.T) {
	secretValue := `not-a-json`
	mockKMS := &MockKMSClient{
		GetSecretValueWithContextFunc: func(ctx context.Context, input *kms.GetSecretValueInput, opts ...request.Option) (*kms.GetSecretValueOutput, error) {
			return &kms.GetSecretValueOutput{
				SecretValue: &secretValue,
			}, nil
		},
	}
	client := NewClient(mockKMS)
	_, err := client.GetSecret(context.Background(), esapi.ExternalSecretDataRemoteRef{
		Key:      "my-secret",
		Property: "prop",
	})
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "failed to unmarshal secret")
}

func TestClient_GetSecret_should_return_error_when_api_call_fails(t *testing.T) {
	mockKMS := &MockKMSClient{
		GetSecretValueWithContextFunc: func(ctx context.Context, input *kms.GetSecretValueInput, opts ...request.Option) (*kms.GetSecretValueOutput, error) {
			return nil, errors.New("api error")
		},
	}
	client := NewClient(mockKMS)
	_, err := client.GetSecret(context.Background(), esapi.ExternalSecretDataRemoteRef{
		Key: "my-secret",
	})
	assert.Error(t, err)
	assert.Equal(t, "api error", err.Error())
}

func TestClient_GetSecret_should_return_error_when_secret_value_is_nil(t *testing.T) {
	mockKMS := &MockKMSClient{
		GetSecretValueWithContextFunc: func(ctx context.Context, input *kms.GetSecretValueInput, opts ...request.Option) (*kms.GetSecretValueOutput, error) {
			return &kms.GetSecretValueOutput{
				SecretValue: nil,
			}, nil
		},
	}
	client := NewClient(mockKMS)
	_, err := client.GetSecret(context.Background(), esapi.ExternalSecretDataRemoteRef{
		Key: "my-secret",
	})
	assert.Error(t, err)
	assert.Equal(t, "secret my-secret has no value", err.Error())
}

func TestClient_GetSecretMap_should_return_map_when_secret_is_valid_json(t *testing.T) {
	secretValue := `{"user":"admin"}`
	mockKMS := &MockKMSClient{
		GetSecretValueWithContextFunc: func(ctx context.Context, input *kms.GetSecretValueInput, opts ...request.Option) (*kms.GetSecretValueOutput, error) {
			return &kms.GetSecretValueOutput{
				SecretValue: &secretValue,
			}, nil
		},
	}
	client := NewClient(mockKMS)
	secretMap, err := client.GetSecretMap(context.Background(), esapi.ExternalSecretDataRemoteRef{
		Key: "my-secret",
	})
	assert.NoError(t, err)
	expectedMap := map[string][]byte{
		"user": []byte(`"admin"`),
	}
	assert.Equal(t, expectedMap, secretMap)
}

func TestClient_GetSecretMap_should_return_error_when_secret_is_not_valid_json(t *testing.T) {
	secretValue := `not-a-json`
	mockKMS := &MockKMSClient{
		GetSecretValueWithContextFunc: func(ctx context.Context, input *kms.GetSecretValueInput, opts ...request.Option) (*kms.GetSecretValueOutput, error) {
			return &kms.GetSecretValueOutput{
				SecretValue: &secretValue,
			}, nil
		},
	}
	client := NewClient(mockKMS)
	_, err := client.GetSecretMap(context.Background(), esapi.ExternalSecretDataRemoteRef{
		Key: "my-secret",
	})
	assert.Error(t, err)
	assert.Contains(t, err.Error(), "failed to unmarshal secret")
}

func TestClient_GetSecretMap_should_return_error_when_api_call_fails(t *testing.T) {
	mockKMS := &MockKMSClient{
		GetSecretValueWithContextFunc: func(ctx context.Context, input *kms.GetSecretValueInput, opts ...request.Option) (*kms.GetSecretValueOutput, error) {
			return nil, errors.New("api error")
		},
	}
	client := NewClient(mockKMS)
	_, err := client.GetSecretMap(context.Background(), esapi.ExternalSecretDataRemoteRef{
		Key: "my-secret",
	})
	assert.Error(t, err)
	assert.Equal(t, "api error", err.Error())
}

func TestClient_SecretExists_should_return_error_when_secret_name_is_empty(t *testing.T) {
	mockKMS := &MockKMSClient{}
	c := NewClient(mockKMS)

	exists, err := c.SecretExists(context.Background(), fakePushScretRemoteRef{
		RemoteKey: "",
	})

	assert.False(t, exists)
	assert.Error(t, err)
	assert.Equal(t, "secret name is empty", err.Error())
}

func TestClient_SecretExists_should_return_error_when_describe_secret_fails(t *testing.T) {
	expectedErr := errors.New("failed to describe secret")
	mockKMS := &MockKMSClient{
		DescribeSecretWithContextFunc: func(ctx context.Context, input *kms.DescribeSecretInput, opts ...request.Option) (*kms.DescribeSecretOutput, error) {
			return nil, expectedErr
		},
	}
	c := NewClient(mockKMS)

	exists, err := c.SecretExists(context.Background(), fakePushScretRemoteRef{
		RemoteKey: "test-secret",
	})

	assert.False(t, exists)
	assert.Error(t, err)
	assert.Equal(t, expectedErr, err)
}

func TestClient_SecretExists_should_return_true_when_secret_exists(t *testing.T) {
	mockKMS := &MockKMSClient{
		DescribeSecretWithContextFunc: func(ctx context.Context, input *kms.DescribeSecretInput, opts ...request.Option) (*kms.DescribeSecretOutput, error) {
			return &kms.DescribeSecretOutput{}, nil
		},
	}
	c := NewClient(mockKMS)

	exists, err := c.SecretExists(context.Background(), fakePushScretRemoteRef{
		RemoteKey: "test-secret",
	})

	assert.True(t, exists)
	assert.NoError(t, err)
}
