package types

import (
	"fmt"

	"github.com/juju/errors"
	"github.com/pingcap/schrodinger/util"
)

// some exported variable
const (
	BinSource = "bin"
	Latest    = "latest"
	GitSource = "git" // require 'Makefile' to build
	GitTypePr = "pr"
)

// TestType defines the type of test
type TestType string

const (
	// PrepareTest do some prepare work before run test.
	// eg: backup cluster / restore cluster / load data
	PrepareTest TestType = "prepare"
	// GeneralTest is the real test to test cluster
	GeneralTest = "test"
)

// TestTypes is a set of TestType
var TestTypes = []TestType{PrepareTest, GeneralTest}

// TestTemplateState is test template state
type TestTemplateState string

// TestTemplateState
const (
	TestTemplateNew      TestTemplateState = "NEW"
	TestTemplateBuilding                   = "BUILDING"
	TestTemplateError                      = "ERROR"
	TestTemplateDone                       = "DONE"
	TestTemplateDeleted                    = "DELETE"
)

// Default test resource
const (
	TestRequestCPUCount    = "0.5"
	TestRequestMemoryCount = "2Gi"

	TestLimitCPUCount    = "1"
	TestLimitMemoryCount = "4Gi"
)

// TestTemplate is the test template
type TestTemplate struct {
	*Meta
	ID             int64                `json:"id"              yaml:"id"`
	Name           string               `json:"name"            yaml:"name"`
	Creator        string               `json:"creator"         yaml:"creator"`
	Ctime          string               `json:"create_time"     yaml:"create_time"`
	Utime          string               `json:"update_time"     yaml:"update_time"`
	Args           string               `json:"args"            yaml:"args"`
	Status         TestTemplateState    `json:"status"          yaml:"status"`
	Type           TestType             `json:"type"            yaml:"type"`
	ErrorInfo      string               `json:"error_info"      yaml:"error_info"`
	ResourceConfig ResourceRequirements `json:"resource_config" yaml:"resource_config"`
	Desc           string               `json:"desc"            yaml:"desc"`
	Source         TestSource           `json:"source"          yaml:"source"`
	Tags           stringSlice          `json:"tags"            yaml:"tags"`

	// the following variables for test from sch-ctl
	// TODO: merge those with old variables
	Environment Environment `json:"environment" yaml:"environment"`
	Manual      Manual      `json:"manual"      yaml:"manual"`
}

// Environment defines some environment informations
type Environment struct {
	Resources ResourceRequirements `json:"resources" yaml:"resources"`
	Image     string               `json:"image"     yaml:"image"`
	Args      string               `json:"args"      yaml:"args"`
}

// Manual describes some usage information
type Manual struct {
	// Description is to introduce the test purpose and instructions for this case.
	Description string `json:"description" yaml:"description" db:"description"`
	// Parameters describes the purpose of the parameters and optional values
	Parameters string `json:"parameters" yaml:"parameters" db:"parameters"`
	// Notes is to list all matters which need to be noted
	Notes string `json:"notes" yaml:"notes" db:"notes"`
}

// Verify verifies manual
func (m *Manual) Verify() error {
	if util.IsEmpty(m.Description) {
		return errors.New("description is not provided")
	}

	return nil
}

var testDefaultConfig = ResourceRequirements{
	Requests: &ResourceRequirement{
		CPU:    TestRequestCPUCount,
		Memory: TestRequestMemoryCount,
	},
	Limits: &ResourceRequirement{
		CPU:    TestLimitCPUCount,
		Memory: TestLimitMemoryCount,
	},
}

// SetDefaultResourceConfig sets default resource for test
func (c *TestTemplate) SetDefaultResourceConfig() {
	c.ResourceConfig.SetDefaultValue(testDefaultConfig)
}

// Verify2 verifies test template from sch-ctl
func (c *TestTemplate) Verify2() error {
	if util.IsEmpty(c.Name) {
		return errors.New("test template name is not provided")
	}

	if util.IsEmpty(c.Creator) {
		return errors.New("test template creator is not provided")
	}

	return c.Manual.Verify()
}

// Verify verifies test template
func (c *TestTemplate) Verify() error {
	if c.Name == "" {
		return errors.BadRequestf("test name can't be empty")
	}

	return c.Source.Verify()
}

// IsNew returns new if test template is new
func (c *TestTemplate) IsNew() bool {
	return c.Status == TestTemplateNew
}

// IsBuilding returns true if test template is build
func (c *TestTemplate) IsBuilding() bool {
	return c.Status == TestTemplateBuilding
}

// IsError returns true if test build error
func (c *TestTemplate) IsError() bool {
	return c.Status == TestTemplateError
}

// IsDone returns true if test build done
func (c *TestTemplate) IsDone() bool {
	return c.Status == TestTemplateDone
}

// IsDeleted returns true if test template is deleted
func (c *TestTemplate) IsDeleted() bool {
	return c.Status == TestTemplateDeleted
}

// ClearErrorInfo clears compile error inf
func (c *TestTemplate) ClearErrorInfo() {
	c.ErrorInfo = ""
}

// SetErrorInfo sets error info
func (c *TestTemplate) SetErrorInfo(info string) {
	c.ErrorInfo = info
}

// TestSource is the test source, support git url, binary, docker
type TestSource struct {
	BinaryName string   `json:"binary_name"`
	Image      string   `json:"image"`
	Type       string   `json:"type"`
	URL        string   `json:"url"`
	GitValue   GitValue `json:"git_value"`
	GitRepo    string   `json:"git_repo"`
}

// GitValue is git value
type GitValue struct {
	GitType string `json:"git_type"`
	Value   string `json:"value"`
}

// Verify verifies test source data
func (c TestSource) Verify() error {
	if c.BinaryName == "" {
		return errors.BadRequestf("source binary name can't be empty")
	}

	if c.Type != BinSource && c.Type != GitSource && c.Type != Latest {
		return errors.NotSupportedf("source type %s", c.Type)
	}

	if (c.Type == BinSource || c.Type == Latest || c.Type == GitSource && c.GitValue.GitType == GitTypePr) &&
		c.URL == "" {
		return errors.BadRequestf("source url is empty")
	}

	if c.Type == GitSource && (c.GitValue.GitType == "") {
		if c.GitValue.GitType == "" {
			return errors.BadRequestf(
				"git source must be defined with starting of : branch, tag or hash")
		}

		if c.GitRepo == "" {
			return errors.BadRequestf("git repo can't be empty")
		}
	}

	return nil
}

// Clone clones test template
func (c *TestTemplate) Clone() *TestTemplate {
	ct := &TestTemplate{}
	if err := util.Clone(ct, c); err != nil {
		return nil
	}
	return ct
}

// TestTemplates is a set of test templates
type TestTemplates []*TestTemplate

// implement sort.Interface
func (ct TestTemplates) Len() int           { return len(ct) }
func (ct TestTemplates) Swap(i, j int)      { ct[i], ct[j] = ct[j], ct[i] }
func (ct TestTemplates) Less(i, j int) bool { return ct[i].Utime > ct[j].Utime }

// Names returns the names
func (ct TestTemplates) Names() []string {
	var names []string
	var exists = make(map[string]bool)

	for _, t := range ct {
		// filter existing names
		if _, exist := exists[t.Name]; !exist {
			names = append(names, t.Name)
			exists[t.Name] = true
		}
	}

	return names
}

// NameWithTags returns test_template VO
func (ct TestTemplates) NameWithTags() []map[string]interface{} {
	var result []map[string]interface{}

	for _, t := range ct {
		item := make(map[string]interface{})
		item["test_name"] = t.Name
		item["test_template_id"] = t.ID
		item["test_args"] = t.Args
		var testTag string

		switch t.Source.Type {
		case BinSource:
			testTag = fmt.Sprintf("bin/%s", t.Source.URL)
		case Latest:
			testTag = Latest
		case GitSource:
			testTag = fmt.Sprintf("%s/%s", t.Source.GitValue.GitType, t.Source.GitValue.Value)
		default:
			testTag = "unknown"
		}

		item["test_tag"] = testTag
		result = append(result, item)
	}

	return result
}

// TestState is test state
type TestState string

// TestState
const (
	TestStatePending   TestState = "PENDING"
	TestStateCreating            = "CREATING"
	TestStateRunning             = "RUNNING"
	TestStateFailed              = "FAILED"
	TestStateStopped             = "STOPPED"
	TestStateFinished            = "FINISHED"
	TestStateDestroyed           = "DESTORYED"
)

// TestProcess is a test instance in a box
type TestProcess struct {
	*Meta
	ID           int64         `json:"id"`
	BoxID        int64         `json:"box_id"`
	ExperimentID int64         `json:"experiment_id"`
	Template     *TestTemplate `json:"template"`
	OverrideArgs *string       `json:"override_args"`
	Cat          *CatProcess   `json:"cat"`
	Ctime        string        `json:"create_time"`
	Utime        string        `json:"update_time"`
	Report       *TestReport   `json:"report"`
	Status       TestState     `json:"status"`
	Mark         Mark          `json:"mark"`
	Namespace    string        `json:"namespace"`
	Name         string        `json:"name"`
}

// Clone clones testProcess
func (c *TestProcess) Clone() *TestProcess {
	cp := &TestProcess{}
	if err := util.Clone(cp, c); err != nil {
		return nil
	}
	return cp
}

// SetState set test state
func (c *TestProcess) SetState(state TestState) {
	c.Status = state
}

// TestProcesses is a set of test process
type TestProcesses []*TestProcess

// implement sort.Interface
func (t TestProcesses) Len() int           { return len(t) }
func (t TestProcesses) Swap(i, j int)      { t[i], t[j] = t[j], t[i] }
func (t TestProcesses) Less(i, j int) bool { return t[i].Utime > t[j].Utime }

// Names returns the names of all test processes
func (t TestProcesses) Names() []string {
	var names []string
	for _, test := range t {
		names = append(names, test.Name)
	}

	return names
}
