package service

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"strings"
	"testing"

	"github.com/stretchr/testify/require"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/serverless-service/internal/domain/constant"
	"github.com/tidbcloud/serverless-service/internal/domain/entity"
	"github.com/tidbcloud/serverless-service/internal/domain/facade/mock_facade"
	"github.com/tidbcloud/serverless-service/internal/domain/vo"
	"go.uber.org/mock/gomock"
)

func TestExportCreatePayloadRoundTrip(t *testing.T) {
	for _, escapeBackslash := range []*bool{nil, boolPtr(false), boolPtr(true)} {
		t.Run(triStateName(escapeBackslash), func(t *testing.T) {
			ctrl := gomock.NewController(t)
			encryptor := mock_facade.NewMockEncryptorFacade(ctrl)
			svc := &exportService{encryptorFacade: encryptor}
			input := &vo.Export{
				TargetType:           constant.ExportTargetTypeS3,
				S3SecretAccessKey:    "s3-secret",
				GCSServiceAccountKey: "gcs-key",
				AzBlobSasToken:       "azure-token",
				OSSAccessKeySecret:   "oss-secret",
				EscapeBackslash:      escapeBackslash,
				ColumnSelectors: []entity.ColumnSelector{{
					Matcher: []string{"app.orders"},
					Columns: []string{"*", "!customer_email"},
				}},
			}

			var plaintext string
			encryptor.EXPECT().EncryptBase64(gomock.Any()).DoAndReturn(func(dataKey string) (string, error) {
				plaintext = dataKey
				return "encrypted-data-key", nil
			})
			encrypted, err := svc.encryptExportCreatePayload(input)
			require.NoError(t, err)
			require.NotContains(t, encrypted, "s3-secret")
			require.Len(t, plaintext, exportCreatePayloadDataKeySize)

			encryptor.EXPECT().DecryptBase64("encrypted-data-key").Return(plaintext, nil)
			output, err := svc.decryptExportCreatePayload(encrypted)
			require.NoError(t, err)
			require.Equal(t, input, output)
		})
	}
}

func TestExportColumnSelectorsPersistRoundTrip(t *testing.T) {
	input := &vo.Export{
		ColumnSelectors: []entity.ColumnSelector{{
			Matcher: []string{"app.orders"},
			Columns: []string{"*", "!customer_email"},
		}},
	}
	stored, err := convertToEntity(context.Background(), input)
	require.NoError(t, err)
	require.Equal(t, input.ColumnSelectors, stored.ColumnSelectors)

	output, err := convertToVO(stored)
	require.NoError(t, err)
	require.Equal(t, input.ColumnSelectors, output.ColumnSelectors)
}

func TestConvertColumnSelectorsToInfraAPI(t *testing.T) {
	selectors := []entity.ColumnSelector{{
		Matcher: []string{"app.orders"},
		Columns: []string{"*", "!customer_email"},
	}}
	require.Equal(t, []cloudv1.ColumnSelector{{
		Matcher: []string{"app.orders"},
		Columns: []string{"*", "!customer_email"},
	}}, convertColumnSelectorsToInfraAPI(selectors))
	require.Nil(t, convertColumnSelectorsToInfraAPI(nil))
}

func TestExportCreatePayloadLargerThanKMSPlaintextLimit(t *testing.T) {
	ctrl := gomock.NewController(t)
	encryptor := mock_facade.NewMockEncryptorFacade(ctrl)
	svc := &exportService{encryptorFacade: encryptor}
	input := &vo.Export{
		TargetType:           constant.ExportTargetTypeGCS,
		GCSServiceAccountKey: strings.Repeat("service-account-key", 512),
		SQL:                  strings.Repeat("SELECT * FROM test; ", 512),
	}
	payload, err := json.Marshal(input)
	require.NoError(t, err)
	require.Greater(t, len(payload), 4096)

	var dataKey string
	encryptor.EXPECT().EncryptBase64(gomock.Any()).DoAndReturn(func(plaintext string) (string, error) {
		dataKey = plaintext
		require.Len(t, plaintext, exportCreatePayloadDataKeySize)
		return "encrypted-data-key", nil
	})
	encrypted, err := svc.encryptExportCreatePayload(input)
	require.NoError(t, err)
	require.NotContains(t, encrypted, input.GCSServiceAccountKey)
	require.NotContains(t, encrypted, input.SQL)

	encryptor.EXPECT().DecryptBase64("encrypted-data-key").Return(dataKey, nil)
	output, err := svc.decryptExportCreatePayload(encrypted)
	require.NoError(t, err)
	require.Equal(t, input, output)
}

func TestDecryptExportCreatePayloadRejectsTamperedCiphertext(t *testing.T) {
	ctrl := gomock.NewController(t)
	encryptor := mock_facade.NewMockEncryptorFacade(ctrl)
	svc := &exportService{encryptorFacade: encryptor}
	input := &vo.Export{
		TargetType:        constant.ExportTargetTypeS3,
		S3SecretAccessKey: "s3-secret",
	}

	var dataKey string
	encryptor.EXPECT().EncryptBase64(gomock.Any()).DoAndReturn(func(plaintext string) (string, error) {
		dataKey = plaintext
		return "encrypted-data-key", nil
	})
	encrypted, err := svc.encryptExportCreatePayload(input)
	require.NoError(t, err)

	var envelope exportCreatePayloadEnvelope
	require.NoError(t, json.Unmarshal([]byte(encrypted), &envelope))
	ciphertext, err := base64.StdEncoding.DecodeString(envelope.Ciphertext)
	require.NoError(t, err)
	ciphertext[len(ciphertext)-1] ^= 0xff
	envelope.Ciphertext = base64.StdEncoding.EncodeToString(ciphertext)
	tampered, err := json.Marshal(envelope)
	require.NoError(t, err)

	encryptor.EXPECT().DecryptBase64("encrypted-data-key").Return(dataKey, nil)
	_, err = svc.decryptExportCreatePayload(string(tampered))
	require.ErrorContains(t, err, "decrypt export create payload")
}

func boolPtr(value bool) *bool {
	return &value
}

func triStateName(value *bool) string {
	if value == nil {
		return "unset"
	}
	if *value {
		return "true"
	}
	return "false"
}
