// Copyright 2015 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,
// See the License for the specific language governing permissions and
// limitations under the License.

package admin_test

import (
	"testing"

	. "github.com/pingcap/check"
	"github.com/pingcap/parser/model"
	"github.com/pingcap/parser/mysql"
	"github.com/pingcap/parser/terror"
	"github.com/pingcap/tidb/kv"
	"github.com/pingcap/tidb/meta"
	"github.com/pingcap/tidb/store/mockstore"
	. "github.com/pingcap/tidb/util/admin"
	"github.com/pingcap/tidb/util/mock"
	"github.com/pingcap/tidb/util/testleak"
)

func TestT(t *testing.T) {
	CustomVerboseFlag = true
	TestingT(t)
}

var _ = Suite(&testSuite{})

type testSuite struct {
	store kv.Storage
	ctx   *mock.Context
}

func (s *testSuite) SetUpSuite(c *C) {
	testleak.BeforeTest()
	var err error
	s.store, err = mockstore.NewMockTikvStore()
	c.Assert(err, IsNil)
	s.ctx = mock.NewContext()
	s.ctx.Store = s.store
}

func (s *testSuite) TearDownSuite(c *C) {
	err := s.store.Close()
	c.Assert(err, IsNil)
	testleak.AfterTest(c)()
}

func (s *testSuite) TestGetDDLInfo(c *C) {
	txn, err := s.store.Begin()
	c.Assert(err, IsNil)
	t := meta.NewMeta(txn)

	dbInfo2 := &model.DBInfo{
		ID:    2,
		Name:  model.NewCIStr("b"),
		State: model.StateNone,
	}
	job := &model.Job{
		SchemaID: dbInfo2.ID,
		Type:     model.ActionCreateSchema,
		RowCount: 0,
	}
	job1 := &model.Job{
		SchemaID: dbInfo2.ID,
		Type:     model.ActionAddIndex,
		RowCount: 0,
	}
	err = t.EnQueueDDLJob(job)
	c.Assert(err, IsNil)
	info, err := GetDDLInfo(txn)
	c.Assert(err, IsNil)
	c.Assert(info.Jobs, HasLen, 1)
	c.Assert(info.Jobs[0], DeepEquals, job)
	c.Assert(info.ReorgHandle, Equals, int64(0))
	// Two jobs.
	t = meta.NewMeta(txn, meta.AddIndexJobListKey)
	err = t.EnQueueDDLJob(job1)
	c.Assert(err, IsNil)
	info, err = GetDDLInfo(txn)
	c.Assert(err, IsNil)
	c.Assert(info.Jobs, HasLen, 2)
	c.Assert(info.Jobs[0], DeepEquals, job)
	c.Assert(info.Jobs[1], DeepEquals, job1)
	c.Assert(info.ReorgHandle, Equals, int64(0))
	err = txn.Rollback()
	c.Assert(err, IsNil)
}

func (s *testSuite) TestGetDDLJobs(c *C) {
	txn, err := s.store.Begin()
	c.Assert(err, IsNil)
	t := meta.NewMeta(txn)
	cnt := 10
	jobs := make([]*model.Job, cnt)
	var currJobs2 []*model.Job
	for i := 0; i < cnt; i++ {
		jobs[i] = &model.Job{
			ID:       int64(i),
			SchemaID: 1,
			Type:     model.ActionCreateTable,
		}
		err = t.EnQueueDDLJob(jobs[i])
		c.Assert(err, IsNil)
		currJobs, err1 := GetDDLJobs(txn)
		c.Assert(err1, IsNil)
		c.Assert(currJobs, HasLen, i+1)
		currJobs2 = currJobs2[:0]
		err = IterAllDDLJobs(txn, func(jobs []*model.Job) (b bool, e error) {
			for _, job := range jobs {
				if job.State == model.JobStateNone {
					currJobs2 = append(currJobs2, job)
				} else {
					return true, nil
				}
			}
			return false, nil
		})
		c.Assert(err, IsNil)
		c.Assert(currJobs2, HasLen, i+1)
	}

	currJobs, err := GetDDLJobs(txn)
	c.Assert(err, IsNil)
	for i, job := range jobs {
		c.Assert(job.ID, Equals, currJobs[i].ID)
		c.Assert(job.SchemaID, Equals, int64(1))
		c.Assert(job.Type, Equals, model.ActionCreateTable)
	}
	c.Assert(currJobs, DeepEquals, currJobs2)

	err = txn.Rollback()
	c.Assert(err, IsNil)
}

func isJobsSorted(jobs []*model.Job) bool {
	if len(jobs) <= 1 {
		return true
	}
	for i := 1; i < len(jobs); i++ {
		if jobs[i].ID <= jobs[i-1].ID {
			return false
		}
	}
	return true
}

func enQueueDDLJobs(c *C, t *meta.Meta, jobType model.ActionType, start, end int) {
	for i := start; i < end; i++ {
		job := &model.Job{
			ID:       int64(i),
			SchemaID: 1,
			Type:     jobType,
		}
		err := t.EnQueueDDLJob(job)
		c.Assert(err, IsNil)
	}
}

func (s *testSuite) TestGetDDLJobsIsSort(c *C) {
	txn, err := s.store.Begin()
	c.Assert(err, IsNil)

	// insert 5 drop table jobs to DefaultJobListKey queue
	t := meta.NewMeta(txn)
	enQueueDDLJobs(c, t, model.ActionDropTable, 10, 15)

	// insert 5 create table jobs to DefaultJobListKey queue
	enQueueDDLJobs(c, t, model.ActionCreateTable, 0, 5)

	// insert add index jobs to AddIndexJobListKey queue
	t = meta.NewMeta(txn, meta.AddIndexJobListKey)
	enQueueDDLJobs(c, t, model.ActionAddIndex, 5, 10)

	currJobs, err := GetDDLJobs(txn)
	c.Assert(err, IsNil)
	c.Assert(currJobs, HasLen, 15)

	isSort := isJobsSorted(currJobs)
	c.Assert(isSort, Equals, true)

	err = txn.Rollback()
	c.Assert(err, IsNil)
}

func (s *testSuite) TestCancelJobs(c *C) {
	txn, err := s.store.Begin()
	c.Assert(err, IsNil)
	t := meta.NewMeta(txn)
	cnt := 10
	ids := make([]int64, cnt)
	for i := 0; i < cnt; i++ {
		job := &model.Job{
			ID:       int64(i),
			SchemaID: 1,
			Type:     model.ActionCreateTable,
		}
		if i == 0 {
			job.State = model.JobStateDone
		}
		if i == 1 {
			job.State = model.JobStateCancelled
		}
		ids[i] = int64(i)
		err = t.EnQueueDDLJob(job)
		c.Assert(err, IsNil)
	}

	errs, err := CancelJobs(txn, ids)
	c.Assert(err, IsNil)
	for i, err := range errs {
		if i == 0 {
			c.Assert(err, NotNil)
			continue
		}
		c.Assert(err, IsNil)
	}

	errs, err = CancelJobs(txn, []int64{})
	c.Assert(err, IsNil)
	c.Assert(errs, IsNil)
	errs, err = CancelJobs(txn, []int64{-1})
	c.Assert(err, IsNil)
	c.Assert(errs[0], NotNil)
	c.Assert(errs[0].Error(), Matches, "*DDL Job:-1 not found")

	// test cancel finish job.
	job := &model.Job{
		ID:       100,
		SchemaID: 1,
		Type:     model.ActionCreateTable,
		State:    model.JobStateDone,
	}
	err = t.EnQueueDDLJob(job)
	c.Assert(err, IsNil)
	errs, err = CancelJobs(txn, []int64{100})
	c.Assert(err, IsNil)
	c.Assert(errs[0], NotNil)
	c.Assert(errs[0].Error(), Matches, "*This job:100 is finished, so can't be cancelled")

	// test can't cancelable job.
	job.Type = model.ActionDropIndex
	job.SchemaState = model.StateWriteOnly
	job.State = model.JobStateRunning
	job.ID = 101
	err = t.EnQueueDDLJob(job)
	c.Assert(err, IsNil)
	errs, err = CancelJobs(txn, []int64{101})
	c.Assert(err, IsNil)
	c.Assert(errs[0], NotNil)
	c.Assert(errs[0].Error(), Matches, "*This job:101 is almost finished, can't be cancelled now")

	// When both types of jobs exist in the DDL queue,
	// we first cancel the job with a larger ID.
	job = &model.Job{
		ID:       1000,
		SchemaID: 1,
		TableID:  2,
		Type:     model.ActionAddIndex,
	}
	job1 := &model.Job{
		ID:       1001,
		SchemaID: 1,
		TableID:  2,
		Type:     model.ActionAddColumn,
	}
	job2 := &model.Job{
		ID:       1002,
		SchemaID: 1,
		TableID:  2,
		Type:     model.ActionAddIndex,
	}
	job3 := &model.Job{
		ID:       1003,
		SchemaID: 1,
		TableID:  2,
		Type:     model.ActionRepairTable,
	}
	err = t.EnQueueDDLJob(job, meta.AddIndexJobListKey)
	c.Assert(err, IsNil)
	err = t.EnQueueDDLJob(job1)
	c.Assert(err, IsNil)
	err = t.EnQueueDDLJob(job2, meta.AddIndexJobListKey)
	c.Assert(err, IsNil)
	err = t.EnQueueDDLJob(job3)
	c.Assert(err, IsNil)
	errs, err = CancelJobs(txn, []int64{job1.ID, job.ID, job2.ID, job3.ID})
	c.Assert(err, IsNil)
	for _, err := range errs {
		c.Assert(err, IsNil)
	}

	err = txn.Rollback()
	c.Assert(err, IsNil)
}

func (s *testSuite) TestGetHistoryDDLJobs(c *C) {
	txn, err := s.store.Begin()
	c.Assert(err, IsNil)
	t := meta.NewMeta(txn)
	cnt := 11
	jobs := make([]*model.Job, cnt)
	for i := 0; i < cnt; i++ {
		jobs[i] = &model.Job{
			ID:       int64(i),
			SchemaID: 1,
			Type:     model.ActionCreateTable,
		}
		err = t.AddHistoryDDLJob(jobs[i], true)
		c.Assert(err, IsNil)
		historyJobs, err1 := GetHistoryDDLJobs(txn, DefNumHistoryJobs)
		c.Assert(err1, IsNil)
		if i+1 > MaxHistoryJobs {
			c.Assert(historyJobs, HasLen, MaxHistoryJobs)
		} else {
			c.Assert(historyJobs, HasLen, i+1)
		}
	}

	delta := cnt - MaxHistoryJobs
	historyJobs, err := GetHistoryDDLJobs(txn, DefNumHistoryJobs)
	c.Assert(err, IsNil)
	c.Assert(historyJobs, HasLen, MaxHistoryJobs)
	l := len(historyJobs) - 1
	for i, job := range historyJobs {
		c.Assert(job.ID, Equals, jobs[delta+l-i].ID)
		c.Assert(job.SchemaID, Equals, int64(1))
		c.Assert(job.Type, Equals, model.ActionCreateTable)
	}

	var historyJobs2 []*model.Job
	err = IterHistoryDDLJobs(txn, func(jobs []*model.Job) (b bool, e error) {
		for _, job := range jobs {
			historyJobs2 = append(historyJobs2, job)
			if len(historyJobs2) == DefNumHistoryJobs {
				return true, nil
			}
		}
		return false, nil
	})
	c.Assert(err, IsNil)
	c.Assert(historyJobs2, DeepEquals, historyJobs)

	err = txn.Rollback()
	c.Assert(err, IsNil)
}

func (s *testSuite) TestIsJobRollbackable(c *C) {
	cases := []struct {
		tp     model.ActionType
		state  model.SchemaState
		result bool
	}{
		{model.ActionDropIndex, model.StateNone, true},
		{model.ActionDropIndex, model.StateDeleteOnly, false},
		{model.ActionDropSchema, model.StateDeleteOnly, false},
		{model.ActionDropColumn, model.StateDeleteOnly, false},
	}
	job := &model.Job{}
	for _, ca := range cases {
		job.Type = ca.tp
		job.SchemaState = ca.state
		re := IsJobRollbackable(job)
		c.Assert(re == ca.result, IsTrue)
	}
}

func (s *testSuite) TestError(c *C) {
	kvErrs := []*terror.Error{
		ErrDataInConsistent,
		ErrDDLJobNotFound,
		ErrCancelFinishedDDLJob,
		ErrCannotCancelDDLJob,
	}
	for _, err := range kvErrs {
		code := terror.ToSQLError(err).Code
		c.Assert(code != mysql.ErrUnknown && code == uint16(err.Code()), IsTrue, Commentf("err: %v", err))
	}
}
