// Copyright 2021 PingCAP, Inc.
//
// 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
//
//     http://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 functionality

import (
	"context"
	"fmt"
	"strings"

	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	. "github.com/pingcap/endless/pkg/matcher"
	. "github.com/pingcap/endless/pkg/workload"
	. "github.com/pingcap/endless/testcase/geopartition/utils"
)

func incompatibleOptionsTest(ctx *SuiteContext) {
	It("Incompatible placement options should fail", func() {
		defer func() {
			MustExec(ctx.DB, "DROP TABLE IF EXISTS t1,t2")
			MustExec(ctx.DB, "DROP PLACEMENT POLICY IF EXISTS p1")
			MustExec(ctx.DB, "DROP PLACEMENT POLICY IF EXISTS p2")
			MustExec(ctx.DB, "DROP PLACEMENT POLICY IF EXISTS pn")
		}()

		MustExec(ctx.DB, "DROP TABLE IF EXISTS t1,t2")
		MustExec(ctx.DB, "DROP PLACEMENT POLICY IF EXISTS p1")
		MustExec(ctx.DB, "DROP PLACEMENT POLICY IF EXISTS p2")
		MustExec(ctx.DB, "DROP PLACEMENT POLICY IF EXISTS pn")
		MustExec(ctx.DB, "CREATE PLACEMENT POLICY p1 "+
			"LEADER_CONSTRAINTS='[+region=us-east-1,+disk=ssd]' "+
			"FOLLOWER_CONSTRAINTS='[+region=us-east-2,+disk=ssd]'",
		)
		MustExec(ctx.DB, "CREATE PLACEMENT POLICY pn PRIMARY_REGION='non-exists' REGIONS='non-exists'")

		iter := Try(NewGenerationForker(generateIncompatibleOptions).DoFork(context.TODO())).(Iterator)
		defer iter.Close()
		cnt := 0
		for iter.Valid() {
			cnt++
			opt := iter.Value().(*incompatibleOption)
			_, err := ctx.DB.Exec(opt.SQL)
			msg := fmt.Sprintf("[%d] Fail for SQL: %s", cnt, opt.SQL)
			Expect(err != nil).Should(BeTrue(), msg)
			if opt.error != "" {
				Expect(err.Error()).Should(Equal(opt.error), msg)
			}
			if opt.errorContains != "" {
				Expect(err.Error()).Should(ContainSubstring(opt.errorContains), msg)
			}
			if opt.errCode != 0 {
				Expect(opt.errCode).Should(Equal(opt.errCode), msg)
			}
			Try(iter.Next())
		}
		Expect(cnt > 0).Should(BeTrue())
	})
}

type incompatibleOption struct {
	SQL           string
	error         string
	errorContains string
	errCode       int
}

func generateIncompatibleOptions(ctx *GenerateContext) (interface{}, error) {
	// Test sugar opt cannot coexist with no sugar opts
	if Try(ctx.PickBool()).(bool) {
		createTableOrPolicy := "CREATE PLACEMENT POLICY p2"
		sugarOpt := Try(ctx.PickStringEnum(
			`PRIMARY_REGION="us-east-1"`,
			`REGIONS="us-east-1"`,
			`PRIMARY_REGION="us-east-1" REGIONS="us-east-1"`,
			`PRIMARY_REGION="us-east-1" REGIONS="us-east-1" FOLLOWERS=1`,
		)).(string)

		noSugarOpt := Try(ctx.PickStringEnum(
			`CONSTRAINTS="[+region=us-east-1]"`,
			"LEADER_CONSTRAINTS=",
			"FOLLOWER_CONSTRAINTS=",
			"LEARNER_CONSTRAINTS=",
			"LEARNERS=1",
		)).(string)

		if strings.HasSuffix(noSugarOpt, "_CONSTRAINTS=") {
			constraint := Try(ctx.PickStringEnum("[+region=us-east-1]", "{+region=us-east-1: 1}")).(string)
			noSugarOpt = fmt.Sprintf("%s\"%s\"", noSugarOpt, constraint)
		}

		return &incompatibleOption{
			SQL:           fmt.Sprintf("%s %s %s", createTableOrPolicy, sugarOpt, noSugarOpt),
			errCode:       1105,
			errorContains: "mixed other sugar options",
		}, nil
	}

	if Try(ctx.PickBool()).(bool) {
		createTableOrPolicy := "CREATE PLACEMENT POLICY p2"
		opt := Try(ctx.PickEnum(
			// Policy has regions not including primary region
			&incompatibleOption{
				SQL:   `PRIMARY_REGION="us-east-1" REGIONS="us-east-2"`,
				error: "Error 1105: invalid placement option: primary region must be included in regions",
			},
			// Primary region without regions
			&incompatibleOption{
				SQL:   `PRIMARY_REGION="us-east-1"`,
				error: "Error 1105: invalid placement option: primary region must be included in regions",
			},
			// Illegal follower count
			&incompatibleOption{
				SQL:   `CONSTRAINTS="[+region=us-east-2]" FOLLOWERS=99`,
				error: "Error 1105: invalid placement option: followers should be less than or equal to 8: 99",
			},
			// Dict syntax but specifying followers
			&incompatibleOption{
				SQL:   `FOLLOWER_CONSTRAINTS="{+region=us-east-2: 2}" FOLLOWERS=4`,
				error: "Error 1105: label constraints with invalid REPLICAS: should not specify replicas=4 when using dict syntax: invalid FollowerConstraints",
			},
			// Dict syntax but specifying learners
			&incompatibleOption{
				SQL:   `LEARNER_CONSTRAINTS="{+region=us-east-2: 2}" LEARNERS=4`,
				error: "Error 1105: label constraints with invalid REPLICAS: should not specify replicas=4 when using dict syntax: invalid LearnerConstraints",
			},
			// Specifying follower constraints as dict but not including a space between the region and count.
			&incompatibleOption{
				SQL:   `LEADER_CONSTRAINTS="[+region=us-east-1]" FOLLOWER_CONSTRAINTS="{+region=us-east-2:2}"`,
				error: "Error 1105: mappings use a colon and space (“: ”) to mark each key/value pair: invalid FollowerConstraints",
			},
			// Specifying follower constraints as dict but the peer count <= 0
			&incompatibleOption{
				SQL:   `FOLLOWER_CONSTRAINTS="{+region=us-east-2: -1}"`,
				error: "Error 1105: label constraints in map syntax have invalid replicas: count of labels '+region=us-east-2' should be positive, but got -1: invalid FollowerConstraints",
			},
			&incompatibleOption{
				SQL:   `FOLLOWER_CONSTRAINTS="{+region=us-east-2: 0}"`,
				error: "Error 1105: label constraints in map syntax have invalid replicas: count of labels '+region=us-east-2' should be positive, but got 0: invalid FollowerConstraints",
			},
			// Specifying learner constraints as dict but the peer count <= 0
			&incompatibleOption{
				SQL:   `LEARNER_CONSTRAINTS="{+region=us-east-2: -1}"`,
				error: "Error 1105: label constraints in map syntax have invalid replicas: count of labels '+region=us-east-2' should be positive, but got -1: invalid LearnerConstraints",
			},
			&incompatibleOption{
				SQL:   `LEARNER_CONSTRAINTS="{+region=us-east-2: 0}"`,
				error: "Error 1105: label constraints in map syntax have invalid replicas: count of labels '+region=us-east-2' should be positive, but got 0: invalid LearnerConstraints",
			},
			// Specifying CONSTRAINTS with dict format
			&incompatibleOption{
				SQL:   `CONSTRAINTS="{+region=us-east-2: 1}"`,
				error: "Error 1105: invalid label constraints format: 'Constraints' should be [constraint1, ...] or any yaml compatible array representation",
			},
			// Specifying LEADER with dict format
			&incompatibleOption{
				SQL:   `LEADER_CONSTRAINTS="{+region=us-east-2: 1}"`,
				error: "Error 1105: invalid label constraints format: 'LeaderConstraints' should be [constraint1, ...] or any yaml compatible array representation",
			},
			&incompatibleOption{
				SQL:   `LEARNER_CONSTRAINTS="[+region=us-east-1]"`,
				error: "Error 1105: invalid placement option: specify learner constraints without specify how many learners to be placed",
			},
		)).(*incompatibleOption)

		return &incompatibleOption{
			SQL:   fmt.Sprintf("%s %s", createTableOrPolicy, opt.SQL),
			error: opt.error,
		}, nil
	}

	// other cases
	return Try(ctx.PickEnum(
		// Create a placement policy with reserved name
		&incompatibleOption{
			SQL:   "CREATE PLACEMENT POLICY `default` FOLLOWERS=2",
			error: "Error 1382: The 'default' syntax is reserved for purposes internal to the MySQL server",
		},
		// Create a table with non exists label
		&incompatibleOption{
			SQL:           `CREATE TABLE t1 (a int) PLACEMENT POLICY pn`,
			errorContains: "can not match any store",
			errCode:       2843,
		},
	)).(*incompatibleOption), nil
}
