package model

import (
	"database/sql/driver"
	"encoding/json"
	"errors"
	"fmt"
	"regexp"

	validation "github.com/Thearas/ozzo-validation/v4"

	"github.com/pingcap/test-infra/common/model/tcms/validator"
)

const (
	caseNameFmt       string = "[a-zA-Z0-9](([_\\-.a-zA-Z0-9]*[a-zA-Z0-9])?)*"
	caseNameErrMsg    string = "case name must consist of alphanumeric characters, '_', '-' or '.', and must start and end with an alphanumeric character"
	caseNameMaxLength int    = 200
)

var (
	caseNameRegex          = regexp.MustCompile("^" + caseNameFmt + "$")
	CaseNameValidationRule = validation.By(func(v interface{}) error {
		if err := validation.Length(1, caseNameMaxLength).Validate(v); err != nil {
			return err
		}
		return validation.Match(caseNameRegex).Error(caseNameErrMsg).Validate(v)
	})
)

// TestCaseBase is the minimum requirements for running a case.
type TestCaseBase struct {
	Name          string             `json:"name" db:"name" yaml:"name"`
	FrameworkType FrameworkType      `json:"-" db:"framework" yaml:"-"`
	Framework     FrameworkUnion     `json:"framework" db:"spec" yaml:"framework"`
	Parameters    CaseParamDeclSlice `json:"parameters" db:"parameters" yaml:"parameters"`
}

func (r TestCaseBase) Validate() error {
	return validation.ValidateStruct(&r,
		validation.Field(&r.Name, validation.Required, CaseNameValidationRule),
		validation.Field(&r.Framework),
		validation.Field(&r.Parameters),
	)
}

// TestCaseMeta holds some meta information about the case.
type TestCaseMeta struct {
	Summary           string               `json:"summary" yaml:"summary" db:"summary"`
	Preconditions     *string              `json:"preconditions,omitempty" yaml:"preconditions,omitempty" db:"preconditions"`
	Steps             *string              `json:"steps,omitempty" yaml:"steps,omitempty" db:"steps"`
	ExpectedResult    *string              `json:"expectedResult,omitempty" yaml:"expectedResult,omitempty" db:"expected_result"`
	Category          CaseCategory         `json:"category" yaml:"category,omitempty" db:"category" default:"FUNCTIONALITY"`
	Priority          CasePriority         `json:"priority" yaml:"priority,omitempty" db:"priority" default:"P1"`
	SupportedVersions StringSlice          `json:"supportedVersions" yaml:"supportedVersions" db:"supported_versions"`
	Automation        CaseAutomationStatus `json:"automation" yaml:"automation" db:"automation" default:"NOT_AUTOMATED"`
	Tags              StringCSV            `json:"tags" yaml:"tags" db:"tags"`
}

func (r TestCaseMeta) Validate() error {
	return validation.ValidateStruct(&r,
		validation.Field(&r.Category),
		validation.Field(&r.Priority),
		validation.Field(&r.SupportedVersions, validation.Each(validator.SemVerConstraint)),
		validation.Field(&r.Automation),
		validation.Field(&r.Tags, validation.Each(validator.Tag)),
	)
}

type TestCase struct {
	TestCaseBase
	TestCaseMeta

	// additional Meta
	ID         ID `json:"id,omitempty" yaml:"id,omitempty" db:"id"`
	DesignedBy `json:"designedBy"`
	CreatedAtAndUpdatedAt
	DeletedAt int64 `json:"-" db:"deleted_at"`
}

type DesignedBy struct {
	ID    ID     `json:"id" db:"designer"`
	Email string `json:"email" db:"designer_email"`
}

// Union types.
type FrameworkUnion struct {
	Generic *GenericFrameworkSpec `json:"generic,omitempty"`
}

func (f *FrameworkUnion) Type() FrameworkType {
	if f.Generic != nil {
		return FrameworkTypeGeneric
	}

	return FrameworkTypeUnknown
}

func (fu *FrameworkUnion) IsValid() bool {
	return fu.Validate() == nil
}

func (v FrameworkUnion) Validate() error {
	if g := v.Generic; g != nil {
		if len(g.Command) < 1 || len(g.Command) > 255 {
			return fmt.Errorf("the command length must be between %d and %d", 1, 255)
		} else if g.Image == "" {
			return errors.New("please specify an case image")
		} else if g.WorkingDir == "" {
			return errors.New("please specify the working directory")
		}
	}
	return nil
}

func (s FrameworkUnion) Value() (driver.Value, error) {
	return json.Marshal(s)
}

func (s *FrameworkUnion) Scan(src interface{}) error {
	var data []byte
	switch v := src.(type) {
	case string:
		data = []byte(v)
	case []byte:
		data = v
	default:
	}
	if len(data) == 0 {
		*s = FrameworkUnion{}
		return nil
	}
	return json.Unmarshal(data, s)
}

type FrameworkType string

const (
	FrameworkTypeUnknown FrameworkType = "UNKNOWN"
	FrameworkTypeGeneric FrameworkType = "GENERIC"
)

type GenericFrameworkSpec struct {
	Command         []string          `json:"command" yaml:"command" validate:"gte=1"`
	WorkingDir      string            `json:"workingDir" yaml:"workingDir" default:"/"`
	Env             map[string]string `json:"env,omitempty" yaml:"env,omitempty"`
	Image           string            `json:"image" yaml:"image" validate:"required"`
	ImagePullPolicy ImagePullPolicy   `json:"imagePullPolicy" yaml:"imagePullPolicy" default:"Always"`
	DefaultTimeout  int               `json:"defaultTimeout" yaml:"defaultTimeout"`
}

type ImagePullPolicy string

const (
	ImagePullPolicyAlways       ImagePullPolicy = "Always"
	ImagePullPolicyIfNotPresent ImagePullPolicy = "IfNotPresent"
	ImagePullPolicyNever        ImagePullPolicy = "Never"
)

func (v ImagePullPolicy) Validate() error {
	values := []ImagePullPolicy{
		ImagePullPolicyAlways,
		ImagePullPolicyIfNotPresent,
		ImagePullPolicyNever,
	}
	for _, value := range values {
		if value == v {
			return nil
		}
	}
	return fmt.Errorf("imagePullPolicy should be one of %v", values)
}

type CaseCategory string

const (
	CaseCategoryFunctionality   CaseCategory = "FUNCTIONALITY"
	CaseCategoryPerformance     CaseCategory = "PERFORMANCE"
	CaseCategoryDurability      CaseCategory = "DURABILITY"
	CaseCategoryReliability     CaseCategory = "RELIABILITY"
	CaseCategoryCompatibility   CaseCategory = "COMPATIBILITY"
	CaseCategoryMaintainability CaseCategory = "MAINTAINABILITY"
	CaseCategorySecurity        CaseCategory = "SECURITY"
	CaseCategoryScene           CaseCategory = "SCENE"
)

func (cc CaseCategory) IsValid() bool {
	return cc.Validate() == nil
}

func (v CaseCategory) Validate() error {
	values := []CaseCategory{
		CaseCategoryFunctionality,
		CaseCategoryPerformance,
		CaseCategoryDurability,
		CaseCategoryReliability,
		CaseCategoryCompatibility,
		CaseCategoryMaintainability,
		CaseCategorySecurity,
		CaseCategoryScene,
	}
	for _, value := range values {
		if value == v {
			return nil
		}
	}
	return fmt.Errorf("case-category should be one of %v", values)
}

type CasePriority string

const (
	CasePriorityP1 CasePriority = "P1"
	CasePriorityP2 CasePriority = "P2"
	CasePriorityP3 CasePriority = "P3"
)

func (cp CasePriority) IsValid() bool {
	return cp.Validate() == nil
}

func (v CasePriority) Validate() error {
	values := []CasePriority{
		CasePriorityP1,
		CasePriorityP2,
		CasePriorityP3,
	}
	for _, value := range values {
		if value == v {
			return nil
		}
	}
	return fmt.Errorf("case-priority should be one of %v", values)
}

type CaseAutomationStatus string

const (
	CaseAutomationStatusNotAutomated CaseAutomationStatus = "NOT_AUTOMATED"
	CaseAutomationStatusInProgress   CaseAutomationStatus = "IN_PROGRESS"
	CaseAutomationStatusAutomated    CaseAutomationStatus = "AUTOMATED"
	CaseAutomationStatusUnable       CaseAutomationStatus = "UNABLE"
)

func (cas CaseAutomationStatus) IsValid() bool {
	return cas.Validate() == nil
}

func (v CaseAutomationStatus) Validate() error {
	values := []CaseAutomationStatus{
		CaseAutomationStatusNotAutomated,
		CaseAutomationStatusInProgress,
		CaseAutomationStatusAutomated,
		CaseAutomationStatusUnable,
	}
	for _, value := range values {
		if value == v {
			return nil
		}
	}
	return fmt.Errorf("case-automation should be one of %v", values)
}

type CaseParamType string

const (
	CaseParamTypeString   CaseParamType = "STRING"
	CaseParamTypeResource CaseParamType = "RESOURCE"
)

func (v CaseParamType) Validate() error {
	values := []CaseParamType{
		CaseParamTypeString,
		CaseParamTypeResource,
	}

	for _, value := range values {
		if value == v {
			return nil
		}
	}
	return fmt.Errorf("case-parameter-type should be one of %v", values)
}

type CaseParamDecl struct {
	Type    CaseParamType `json:"type" yaml:"type" default:"STRING"`
	Name    string        `json:"name" yaml:"name"`
	Default string        `json:"default,omitempty" yaml:"default,omitempty"`
}

func (v CaseParamDecl) Validate() error {
	return validation.ValidateStruct(&v,
		validation.Field(&v.Type),
		validation.Field(&v.Name, validation.Required, validator.ParameterName),
	)
}

// CaseParamDeclSlice used for mapping column in DB.
type CaseParamDeclSlice []CaseParamDecl

func (s CaseParamDeclSlice) Value() (driver.Value, error) {
	if len(s) == 0 {
		return []byte("[]"), nil
	}
	return json.Marshal(s)
}

func (s *CaseParamDeclSlice) Scan(src interface{}) error {
	var data []byte
	switch v := src.(type) {
	case string:
		data = []byte(v)
	case []byte:
		data = v
	default:
	}
	if len(data) == 0 {
		*s = []CaseParamDecl{}
		return nil
	}
	return json.Unmarshal(data, s)
}

// CaseParamBindSlice is a []CaseParamBind but stores as a JSON array in the database.
type CaseParamBindSlice []CaseParamBind

func (s CaseParamBindSlice) Value() (driver.Value, error) {
	if len(s) == 0 {
		return []byte("[]"), nil
	}
	return json.Marshal(s)
}

func (s *CaseParamBindSlice) Scan(src interface{}) error {
	var data []byte
	switch v := src.(type) {
	case string:
		data = []byte(v)
	case []byte:
		data = v
	default:
	}
	if len(data) == 0 {
		*s = []CaseParamBind{}
		return nil
	}
	return json.Unmarshal(data, s)
}

type CaseParamBind struct {
	Name  string `json:"name" yaml:"name"`
	Value string `json:"value" yaml:"value"`
}

func (v CaseParamBind) Validate() error {
	return validation.ValidateStruct(&v,
		validation.Field(&v.Name, validation.Required, validator.ParameterName),
	)
}

type CaseIDAndName struct {
	ID   ID     `json:"id" db:"case_id"`
	Name string `json:"name" db:"case_name"`
}
