package storage

import (
	"encoding/json"
	"reflect"
	"testing"
	"time"

	"github.com/juju/errors"
	. "github.com/pingcap/check"
	"github.com/pingcap/schrodinger/pkg/types"
)

var _ = Suite(&boxSuite{})

type boxSuite struct {
	store *Storage
}

func (t *boxSuite) SetUpTest(c *C) {
	t.store = setupStore("box")
}

func (t *boxSuite) TearDownSuite(c *C) {
	t.store.Close()
}

func (t *boxSuite) TestCreateBox(c *C) {
	if err := t.store.CreateBox(insertBox); err != nil {
		c.Fatal("creating box fails:", errors.ErrorStack(err))
	}
	boxes, _ := t.store.ListBox(nil)
	if len(boxes) != 1 {
		c.Fatal("expect one element in boxes")
	}
	got := boxes[0]

	if !reflect.DeepEqual(insertBox, got) {
		c.Fatalf("expect %v, got %v", *insertBox, *got)
	}
}

func (t *boxSuite) TestListBox(c *C) {
	t.TestCreateBox(c)

	boxes, _ := t.store.ListBox(nil)
	if len(boxes) != 1 {
		c.Fatal("expect one element in boxes")
	}

	got := boxes[0]
	if !reflect.DeepEqual(insertBox, got) {
		c.Fatalf("expect %v, got %v", *insertBox, got)
	}
}

func (t *boxSuite) GetBoxPagination(c *C) {
	t.TestCreateBox(c)
	got, _ := t.store.ListBox(nil)

	c.Assert(types.Boxes{insertBox}, DeepEquals, got)
}

func (t *boxSuite) TestGetBox(c *C) {
	t.TestCreateBox(c)

	got, exists := t.store.GetBox(insertBox.ID)
	if !exists {
		c.Fatalf("get box id `%d` fails:", insertBox.ID)
	}
	if !reflect.DeepEqual(insertBox, got) {
		c.Fatalf("expect %v, got %v", *insertBox, *got)
	}

	got, exists = t.store.GetBox(1000)
	if exists {
		c.Fatalf("expect not exist")
	}
}

func (t *boxSuite) TestGetBoxByName(c *C) {
	t.TestCreateBox(c)

	got, exists := t.store.GetBoxByName(insertBox.Name)
	if !exists {
		c.Fatalf("get box by name `%s` fails", insertBox.Name)
	}
	if !reflect.DeepEqual(insertBox, got) {
		c.Fatalf("expect %v, got %v", *insertBox, *got)
	}

	got, exists = t.store.GetBoxByName("not exist name")
	if exists {
		c.Fatalf("expect not exists")
	}
}

func (t *boxSuite) TestUpdateBox(c *C) {
	t.TestCreateBox(c)

	updateBox.ID = insertBox.ID
	updateBox.Ctime = insertBox.Ctime

	if err := t.store.UpdateBox(updateBox); err != nil {
		c.Fatal("updating box fails:", errors.ErrorStack(err))
	}
	boxes, _ := t.store.ListBox(nil)
	if len(boxes) != 1 {
		c.Fatal("expect one element in boxes")
	}
	got := boxes[0]

	if !reflect.DeepEqual(updateBox, got) {
		d1, _ := json.Marshal(updateBox)
		d2, _ := json.Marshal(got)
		c.Fatalf("expect %v, got %v", string(d1), string(d2))
	}
}

func (t *boxSuite) TestGetBoxByTag(c *C) {
	t.TestUpdateBox(c)

	boxes := t.store.GetBoxesByTag("tag1")
	if len(boxes) != 1 {
		c.Fatalf("expect one element in boxes, got %v", boxes)
	}

	got := boxes[0]
	if !reflect.DeepEqual(updateBox, got) {
		c.Fatalf("expect %v, got %v", *insertBox, got)
	}

	boxes = t.store.GetBoxesByTag("tag4")
	if len(boxes) != 0 {
		c.Fatalf("expect zero elements in boxes, got %v", boxes)
	}
}

func (t *boxSuite) TestDeleteBox(c *C) {
	t.TestCreateBox(c)

	if err := t.store.DeleteBox(insertBox.ID); err != nil {
		c.Fatalf("unexpect error: %s", errors.ErrorStack(err))
	}
	got, exists := t.store.GetBox(insertBox.ID)
	c.Assert(exists, Equals, true)
	if got.State != types.BoxStateDeleted {
		c.Fatalf("expect types.BoxStateDeleted, got %v", got.State)
	}
}

func (t *boxSuite) TestStopBox(c *C) {
	t.TestCreateBox(c)

	if err := t.store.StopBox(insertBox.ID); err != nil {
		c.Fatalf("unexpect error: %s", errors.ErrorStack(err))
	}
	got, exists := t.store.GetBox(insertBox.ID)
	c.Assert(exists, Equals, true)
	if got.State != types.BoxStateOffline {
		c.Fatalf("expect types.BoxStateOffline, got %v", got.State)
	}
}

func (t *boxSuite) TestGetBoxExperimentCount(c *C) {
	t.TestCreateBox(c)

	count, err := t.store.GetBoxExperimentCount(insertBox.ID)
	c.Assert(count, Equals, insertBox.ExperimentCount)
	c.Assert(err, Equals, nil)
}

func (t *boxSuite) TestIncBoxExperimentCount(c *C) {
	t.TestCreateBox(c)
	count, err := t.store.IncBoxExperimentCount(insertBox.ID)
	c.Assert(count, Equals, insertBox.ExperimentCount+1)
	c.Assert(err, Equals, nil)

	got, exists := t.store.GetBox(insertBox.ID)
	c.Assert(exists, Equals, true)
	c.Assert(got.ExperimentCount, Equals, insertBox.ExperimentCount+1)
}

func TestGenBoxSelectStmt(t *testing.T) {
	got := genBoxSelectStmt(nil)
	expect := boxSelect + `WHERE status != 'DELETED'
ORDER BY create_time DESC
`
	if got != expect {
		t.Fatalf("expect %s, got %s", expect, got)
	}
	got = genBoxSelectStmt(&BoxConditions{})
	expect = boxSelect + `WHERE 1 = 1 AND status != 'DELETED'
ORDER BY create_time DESC
`
	if got != expect {
		t.Fatalf("expect %s, got %s", expect, got)
	}
	got = genBoxSelectStmt(&BoxConditions{Names: []string{}, BoxType: []string{"Hello", "world"}, Limit: 10, Offset: 0})
	expect = boxSelect + `WHERE 1 = 1 AND box.box_type IN ('Hello', 'world') AND status != 'DELETED'
ORDER BY create_time DESC
LIMIT 10 OFFSET 0`
	if got != expect {
		t.Fatalf("expect %s, got %s", expect, got)
	}
	got = genBoxSelectStmt(&BoxConditions{Names: []string{}, BoxType: []string{"Hello", "world"}, Limit: 10, Offset: 0,
		ExpStage:      []string{"PENDING", "PREPARING"},
		ExpStatus:     []string{"NORMAL", "ABNORMAL"},
		LimitEveryBox: 10,
	})
	expect = boxSelect + `LEFT JOIN (
    SELECT id, box, stage, state, destroyed
    FROM experiment
    WHERE (
        SELECT COUNT(*)
        FROM experiment
        AS f
        WHERE
            f.box = experiment.box
        AND f.create_time >= experiment.create_time) <= 10
) AS experiment
ON box.id = experiment.box
WHERE 1 = 1 AND experiment.stage IN ('PENDING', 'PREPARING') AND experiment.state IN ('NORMAL', 'ABNORMAL') AND box.box_type IN ('Hello', 'world')
AND box.status != 'DELETED'
GROUP BY box.id
HAVING (COUNT(experiment.id)) > 0
LIMIT 10 OFFSET 0`
	if got != expect {
		t.Fatalf("expect %s, got %s", expect, got)
	}
}

func TestGenBoxCountStmt(t *testing.T) {

	for index, testCase := range []struct {
		conditions *BoxConditions
		expect     string
	}{
		{&BoxConditions{
			BoxType:       []string{},
			Names:         []string{""},
			ExpDestroyed:  []string{},
			LimitEveryBox: 10,
			Limit:         20,
			Offset:        20,
		}, boxCount + `WHERE 1 = 1 AND box.name LIKE '%%' AND status != 'DELETED'
ORDER BY create_time DESC
`}, {
			&BoxConditions{
				ExpDestroyed:  []string{},
				ExpStage:      []string{"PENDING", "PREPARING"},
				ExpStatus:     []string{"NORMAL", "ABNORMAL"},
				LimitEveryBox: 10,
				Limit:         20,
				Offset:        20,
			}, `SELECT COUNT(*)
FROM (
    SELECT box.*
    FROM box
    LEFT JOIN (
        SELECT id, box, stage, state, destroyed
        FROM experiment
        WHERE (
            SELECT COUNT(*) FROM experiment AS f
            WHERE f.box = experiment.box
                AND f.create_time >= experiment.create_time
            ) <= 10
    ) AS experiment
    ON box.id = experiment.box
    WHERE 1 = 1 AND experiment.stage IN ('PENDING', 'PREPARING') AND experiment.state IN ('NORMAL', 'ABNORMAL')
    AND box.status != 'DELETED'
    GROUP BY box.id
    HAVING (count(experiment.id)) > 0
) AS tmp`,
		},
	} {
		got := genBoxCountStmt(testCase.conditions)
		expect := testCase.expect
		if got != expect {
			t.Fatalf("[index: %d] expect %s, got %s", index, expect, got)
		}
	}
}

var insertBox = &types.Box{
	Name:                "test-box",
	Creator:             "mahjonp",
	Desc:                "test-box desc",
	State:               types.BoxStateOnline,
	Tags:                nil,
	ExperimentCount:     2,
	NodeLabelsForUpdate: map[string]string{"key": "val"},
	WithoutCat:          true,
	Rules:               []types.Rule{{Type: "rule type", Value: "rule value"}},
	Tests:               &types.TestsConfig{InOrder: true, Tests: types.TestConfigs{types.TestConfig{TestName: "test1", TestTemplateID: 1}, types.TestConfig{TestName: "test2", TestTemplateID: 2}}, LabelStr: "test label"},
	Misc:                &types.MiscConfig{SlackChannelForError: "#tid-b", SlackChannelForSuccess: "#tid-b", DestroyTidbCluster: true, Timeout: time.Minute, Type: "general"},
}

var updateBox = &types.Box{
	Name:                "test-box2",
	Creator:             "mahjonp2",
	Desc:                "test-box desc2",
	State:               types.BoxStateOffline,
	Tags:                []string{"tag1", "tag2", "tag3"},
	ExperimentCount:     3,
	NodeLabelsForUpdate: map[string]string{"key2": "val2"},
	Cat: &types.Cat{
		LabelStr:    "cat label2",
		SelectedCat: "cat selected cat2",
		Name:        "cat name2",
		Creator:     "mahjonp2",
		PDConfig: types.ComponentConfig{
			Size: 1,
			Version: types.VersionInfo{
				Type:          "pd type2",
				Value:         "pd value2",
				Platform:      "pd platform2",
				CompileOption: "pd compileOption2",
				BuildTime:     "2019-02-20 18:01",
			},
			ConfigTemplateName: "pd config template name2",
			LogLevel:           "info",
			ResourceConfig: types.ResourceRequirements{
				Requests: &types.ResourceRequirement{
					CPU:          "200",
					Memory:       "5Gb",
					StorageClass: "sata",
				},
				Limits: &types.ResourceRequirement{
					CPU:          "2000",
					Memory:       "9Gb",
					StorageClass: "sata",
				},
			},
			Fuse: false,
			Args: "--test=true",
		},
		TiKVConfig: types.ComponentConfig{
			Size: 1,
			Version: types.VersionInfo{
				Type:          "tikv type2",
				Value:         "tikv value2",
				Platform:      "tikv platform2",
				CompileOption: "tikv compileOption2",
				BuildTime:     "2019-02-20 18:01",
			},
			ConfigTemplateName: "tikv config template name2",
			LogLevel:           "info",
			ResourceConfig: types.ResourceRequirements{
				Requests: &types.ResourceRequirement{
					CPU:          "200",
					Memory:       "5Gb",
					StorageClass: "sata",
				},
				Limits: &types.ResourceRequirement{
					CPU:          "2000",
					Memory:       "9Gb",
					StorageClass: "sata",
				},
			},
			Fuse: false,
			Args: "--test=true",
		},
		TiDBConfig: types.ComponentConfig{
			Size: 1,
			Version: types.VersionInfo{
				Type:          "tidb type2",
				Value:         "tidb value2",
				Platform:      "tidb platform2",
				CompileOption: "tidb compileOption2",
				BuildTime:     "2019-02-20 18:01",
			},
			ConfigTemplateName: "tidb config template name2",
			LogLevel:           "info",
			ResourceConfig: types.ResourceRequirements{
				Requests: &types.ResourceRequirement{
					CPU:          "200",
					Memory:       "5Gb",
					StorageClass: "sata",
				},
				Limits: &types.ResourceRequirement{
					CPU:          "2000",
					Memory:       "9Gb",
					StorageClass: "sata",
				},
			},
			Fuse: false,
			Args: "--test=true",
		},
		Monitor: false,
	},
	WithoutCat: false,
	Rules:      []types.Rule{{Type: "rule type2", Value: "rule value2"}},
	Tests:      &types.TestsConfig{InOrder: false, Tests: types.TestConfigs{types.TestConfig{TestName: "test1", TestTemplateID: 3}}, LabelStr: "test label2"},
	Misc:       &types.MiscConfig{SlackChannelForError: "#tidb", SlackChannelForSuccess: "#tidb", DestroyTidbCluster: false, Timeout: time.Hour, Type: "bench"},
}
