package engine

import (
	"errors"
	"fmt"
	"testing"

	"cloud.google.com/go/bigquery"
	"github.com/aws/aws-sdk-go-v2/service/athena/types"
	mssql "github.com/microsoft/go-mssqldb"
	"github.com/stretchr/testify/require"
	"google.golang.org/api/googleapi"
)

func TestUnknownFieldErrorNormalizesFields(t *testing.T) {
	err, ok := AsUnknownFieldError(fmt.Errorf("wrapped: %w", &UnknownFieldError{
		Engine: "test", Fields: []string{"tikv_cpu_time", "tidb_cpu_time", "tikv_cpu_time", "unsafe.field"},
	}))
	require.True(t, ok)
	require.Equal(t, []string{"tidb_cpu_time", "tikv_cpu_time"}, err.Fields)
	require.NotContains(t, err.Error(), "unsafe")
}

func TestClassifyBigQueryUnknownField(t *testing.T) {
	err := bigquery.MultiError{
		&bigquery.Error{Reason: "invalidQuery", Message: "Unrecognized name: tikv_cpu_time at [1:8]"},
		&bigquery.Error{Reason: "invalidQuery", Message: "Unrecognized name: tidb_cpu_time; Did you mean cpu_time? at [2:3]"},
	}
	unknown, ok := AsUnknownFieldError(classifyBigQueryUnknownField(err, "job-1"))
	require.True(t, ok)
	require.Equal(t, "job-1", unknown.QueryID)
	require.Equal(t, []string{"tidb_cpu_time", "tikv_cpu_time"}, unknown.Fields)

	require.Nil(t, classifyBigQueryUnknownField(&bigquery.Error{Reason: "backendError", Message: "Unrecognized name: secret"}, ""))
	require.Nil(t, classifyBigQueryUnknownField(&bigquery.Error{Reason: "invalidQuery", Message: "Syntax error: unexpected keyword"}, ""))
	require.Nil(t, classifyBigQueryUnknownField(&bigquery.Error{Reason: "invalidQuery", Message: "Unrecognized name: unsafe.field at [1:8]"}, ""))
	require.Nil(t, classifyBigQueryUnknownField(&bigquery.Error{Reason: "invalidQuery", Message: `Unrecognized name: "unsafe" at [1:8]`}, ""))
	require.Nil(t, classifyBigQueryUnknownField(&bigquery.Error{Reason: "invalidQuery", Message: "Unrecognized name: path/to/field at [1:8]"}, ""))
}

func TestClassifyBigQueryStatusUnknownFieldRequiresPrimaryMatch(t *testing.T) {
	primary := &bigquery.Error{Reason: "invalidQuery", Message: "Name tidb_cpu_time not found inside sq at [1:8]"}
	supplemental := []*bigquery.Error{
		{Reason: "invalidQuery", Message: "Unrecognized name: tikv_cpu_time at [1:8]"},
		{Reason: "invalidQuery", Message: "Unrecognized name: tidb_cpu_time at [1:8]"},
	}
	unknown, ok := AsUnknownFieldError(classifyBigQueryStatusUnknownField(primary, supplemental, "job-2"))
	require.True(t, ok)
	require.Equal(t, []string{"tidb_cpu_time", "tikv_cpu_time"}, unknown.Fields)
	require.Equal(t, "job-2", unknown.QueryID)

	nonUnknownPrimary := &bigquery.Error{Reason: "invalidQuery", Message: "Syntax error: unexpected keyword"}
	require.Nil(t, classifyBigQueryStatusUnknownField(nonUnknownPrimary, supplemental, "job-3"))
}

func TestClassifyBigQueryWaitUnknownField(t *testing.T) {
	err := fmt.Errorf("wrapped: %w", &googleapi.Error{
		Code: 400,
		Errors: []googleapi.ErrorItem{
			{Reason: "invalidQuery", Message: "Unrecognized name: tikv_cpu_time at [1:8]"},
			{Reason: "invalidQuery", Message: "Unrecognized name: tidb_cpu_time; Did you mean cpu_time? at [2:3]"},
		},
	})
	unknown, ok := AsUnknownFieldError(classifyBigQueryWaitUnknownField(err, "job-wait"))
	require.True(t, ok)
	require.Equal(t, "job-wait", unknown.QueryID)
	require.Equal(t, []string{"tidb_cpu_time", "tikv_cpu_time"}, unknown.Fields)
}

func TestClassifyBigQueryWaitUnknownFieldStrictNegatives(t *testing.T) {
	tests := []*googleapi.Error{
		{Code: 500, Errors: []googleapi.ErrorItem{{Reason: "invalidQuery", Message: "Unrecognized name: tidb_cpu_time"}}},
		{Code: 400},
		{Code: 400, Errors: []googleapi.ErrorItem{{Reason: "backendError", Message: "Unrecognized name: tidb_cpu_time"}}},
		{Code: 400, Errors: []googleapi.ErrorItem{
			{Reason: "invalidQuery", Message: "Unrecognized name: tidb_cpu_time"},
			{Reason: "backendError", Message: "backend failure"},
		}},
		{Code: 400, Errors: []googleapi.ErrorItem{{Reason: "invalidQuery", Message: "Syntax error: unexpected keyword"}}},
		{Code: 400, Errors: []googleapi.ErrorItem{{Reason: "invalidQuery", Message: "Unrecognized name: unsafe.field at [1:8]"}}},
		{Code: 400, Errors: []googleapi.ErrorItem{{Reason: "invalidQuery", Message: `Unrecognized name: "unsafe" at [1:8]`}}},
	}
	for _, err := range tests {
		require.Nil(t, classifyBigQueryWaitUnknownField(err, "job-negative"), "%+v", err.Errors)
	}
}

func TestBigQueryQueryErrorContainsOnlySafeDiagnostics(t *testing.T) {
	raw := &bigquery.Error{
		Reason:  "invalidQuery",
		Message: "Unrecognized name: customer_secret; Did you mean private_field? at [1:8]",
	}
	err := newBigQueryQueryError("job-4", raw, []*bigquery.Error{raw})
	require.Equal(t, "job-4", err.QueryID)
	require.Equal(t, bigQueryErrorSourceBigQuery, err.ErrorSource)
	require.Equal(t, "invalidQuery", err.Reason)
	require.Equal(t, 1, err.ErrorCount)
	require.Equal(t, bigQueryMessageShapeUnrecognizedName, err.MessageShape)
	require.True(t, err.HasSuggestion)
	require.NotContains(t, fmt.Sprintf("%+v", err), "customer_secret")
	require.NotContains(t, fmt.Sprintf("%+v", err), "private_field")

	unsafeReason := newBigQueryQueryError("job-5", &bigquery.Error{
		Reason:  "invalidQuery customer-data",
		Message: "Syntax error",
	}, nil)
	require.Equal(t, "unknown", unsafeReason.Reason)

	googleErr := newBigQueryQueryError("job-6", &googleapi.Error{
		Code: 400,
		Errors: []googleapi.ErrorItem{{
			Reason:  "invalidQuery",
			Message: "Unrecognized name: customer_secret; Did you mean private_field?",
		}},
		Body: "customer response body",
	}, nil)
	require.Equal(t, bigQueryErrorSourceGoogleAPI, googleErr.ErrorSource)
	require.Equal(t, "invalidQuery", googleErr.Reason)
	require.Equal(t, 1, googleErr.ErrorCount)
	require.Equal(t, bigQueryMessageShapeUnrecognizedName, googleErr.MessageShape)
	require.True(t, googleErr.HasSuggestion)
	require.NotContains(t, fmt.Sprintf("%+v", googleErr), "customer_secret")
	require.NotContains(t, fmt.Sprintf("%+v", googleErr), "private_field")
	require.NotContains(t, fmt.Sprintf("%+v", googleErr), "customer response body")
}

func TestBigQueryMessageShapeUsesFixedCategories(t *testing.T) {
	tests := []struct {
		message string
		shape   string
	}{
		{message: "", shape: bigQueryMessageShapeEmpty},
		{message: "Unrecognized name: tidb_cpu_time at [1:8]", shape: bigQueryMessageShapeUnrecognizedName},
		{message: "Unrecognized name: tidb_cpu_time; Did you mean cpu_time?", shape: bigQueryMessageShapeUnrecognizedName},
		{message: "Name tidb_cpu_time not found inside sq at [1:8]", shape: bigQueryMessageShapeNameNotFoundInside},
		{message: "Cannot query over table redacted without a filter over column(s) date", shape: bigQueryMessageShapePartitionFilter},
		{message: "Table redacted must be qualified with a dataset", shape: bigQueryMessageShapeDatasetRequired},
		{message: "Dataset redacted was not found", shape: bigQueryMessageShapeDatasetNotFound},
		{message: "Resource is not available in this location", shape: bigQueryMessageShapeLocation},
		{message: "Error while reading table: redacted, failed to expand table redacted with file pattern redacted", shape: bigQueryMessageShapeFilePattern},
		{message: "Syntax error containing customer data", shape: bigQueryMessageShapeOther},
	}
	for _, test := range tests {
		require.Equal(t, test.shape, bigQueryMessageShape(test.message))
	}
}

func TestAsBigQueryQueryErrorNormalizesDiagnosticEnums(t *testing.T) {
	diagnostics, ok := AsBigQueryQueryError(&BigQueryQueryError{
		QueryID:       "customer/query",
		ErrorSource:   "customer-source",
		Reason:        "customer reason",
		ErrorCount:    -1,
		MessageShape:  "customer-shape",
		HasSuggestion: true,
	})
	require.True(t, ok)
	require.Empty(t, diagnostics.QueryID)
	require.Equal(t, bigQueryErrorSourceUnknown, diagnostics.ErrorSource)
	require.Equal(t, "unknown", diagnostics.Reason)
	require.Zero(t, diagnostics.ErrorCount)
	require.Equal(t, bigQueryMessageShapeOther, diagnostics.MessageShape)
	require.True(t, diagnostics.HasSuggestion)
}

func TestClassifyAthenaUnknownField(t *testing.T) {
	err := &AthenaQueryError{
		QueryID: "athena-1", State: types.QueryExecutionStateFailed,
		Reason: "COLUMN_NOT_FOUND: line 1:8: Column 'tidb_cpu_time' cannot be resolved",
	}
	unknown, ok := AsUnknownFieldError(classifyAthenaUnknownField(fmt.Errorf("wrapped: %w", err)))
	require.True(t, ok)
	require.Equal(t, "athena-1", unknown.QueryID)
	require.Equal(t, []string{"tidb_cpu_time"}, unknown.Fields)

	require.Nil(t, classifyAthenaUnknownField(&AthenaQueryError{Reason: "SYNTAX_ERROR: mismatched input"}))
	require.Nil(t, classifyAthenaUnknownField(&AthenaQueryError{Reason: "INVALID_GLUE_SCHEMA: schema mismatch"}))
	require.Nil(t, classifyAthenaUnknownField(&AthenaQueryError{Reason: "INVALID_TABLE_PROPERTY: property: projection.instance.values"}))
}

func TestClassifyMaxComputeUnknownField(t *testing.T) {
	unknown, ok := AsUnknownFieldError(classifyMaxComputeUnknownField(
		errors.New("FAILED: ODPS-0130071:[73,12] Semantic analysis exception - column tidb_cpu_time cannot be resolved"), "instance-1"))
	require.True(t, ok)
	require.Equal(t, []string{"tidb_cpu_time"}, unknown.Fields)
	require.Equal(t, "instance-1", unknown.QueryID)

	negative := []string{
		"ODPS-0130071:[1,1] Semantic analysis exception - expression not in GROUP BY key",
		"ODPS-0130071:[1,1] Semantic analysis exception - partition not found",
		"ODPS-0130071:[1,1] Semantic analysis exception - evaluate function does not match annotation",
		"ODPS-0130071:[1,1] Semantic analysis exception - wrong columns count",
	}
	for _, message := range negative {
		require.Nil(t, classifyMaxComputeUnknownField(errors.New(message), ""), message)
	}
}

func TestClassifySynapseUnknownField(t *testing.T) {
	err := mssql.Error{Number: 207, Message: "Invalid column name 'tikv_cpu_time'.", All: []mssql.Error{
		{Number: 207, Message: "Invalid column name 'tikv_cpu_time'."},
		{Number: 207, Message: "Invalid column name 'tidb_cpu_time'."},
	}}
	unknown, ok := AsUnknownFieldError(classifySynapseUnknownField(fmt.Errorf("wrapped: %w", err)))
	require.True(t, ok)
	require.Equal(t, []string{"tidb_cpu_time", "tikv_cpu_time"}, unknown.Fields)
	require.Nil(t, classifySynapseUnknownField(mssql.Error{Number: 208, Message: "Invalid object name 'table'"}))
}
