package handler

import (
	"fmt"
	"net/http"
	"strconv"
	"time"

	junit "github.com/jstemmer/go-junit-report/formatter"
	"github.com/labstack/echo/v4"

	"github.com/pingcap/test-infra/common/model"
	"github.com/pingcap/test-infra/services/tcms/service/common"
	serror "github.com/pingcap/test-infra/services/tcms/service/common/errors"
	"github.com/pingcap/test-infra/services/tcms/utils"
)

type TriggerTestsResp struct {
	ID             int64 `json:"id"`
	TriggeredCount int   `json:"triggeredCount"`
}

// TriggerDailyTestsByDailyCI ...
// @Summary Trigger daily tests (Used by dailyci).
// @ID trigger-daily-tests-by-dailyci
// @Tags dailyci
// @Success 200 {object} TriggerTestsResp
// @Failure default {object} HTTPError
// @Router /dailyci/trigger [post]
func (h *HTTPHandler) TriggerDailyTestsByDailyCI(c echo.Context) (err error) {
	ctx := c.(utils.Context)

	// We need to get plan-execution by time with triggerAt.Unix()
	// While there is a bug described here:
	// https://github.com/pingcap/test-infra/issues/796
	// To fix it we need to add 'Truncate(time.Second)' here
	triggerAt := time.Now().Truncate(time.Second)
	ctx.Log().With("triggerAt", triggerAt.Unix()).Infof("dailyci: trigger daily tests")

	count := 0
	maxLoop := 100
	for i := 0; i < maxLoop; i++ {
		triggeredCount, err := h.service.Trigger.Trigger(ctx, model.TriggerTypeCron, triggerAt, 10)
		if err != nil {
			ctx.Log().Errorf(err, "trigger daily tests failed")
			return err
		}

		count += triggeredCount
		if triggeredCount == 0 {
			// no more daily test needed to be triggered
			break
		}
	}

	return ctx.JSON(http.StatusOK, TriggerTestsResp{ID: triggerAt.Unix(), TriggeredCount: count})
}

type GetTriggerStatusForDailyCIResp struct {
	Finished bool `json:"finished"`
}

// GetTriggerStatusForDailyCI ...
// @Summary Trigger daily tests (Used by dailyci).
// @ID get-trigger-status-for-dailyci
// @Tags dailyci
// @Param id path integer true "id"
// @Success 200 {object} GetTriggerStatusForDailyCIResp
// @Failure default {object} HTTPError
// @Router /dailyci/trigger/{id} [get]
func (h *HTTPHandler) GetTriggerStatusForDailyCI(c echo.Context) (err error) {
	ctx := c.(utils.Context)

	id, err := strconv.ParseInt(ctx.Param("id"), 10, 64)
	if err != nil {
		return serror.New(http.StatusBadRequest, err)
	}

	finished, err := h.service.PlanExec.IsAllFinished(ctx, time.Unix(id, 0))
	if err != nil {
		ctx.Log().Errorf(err, "GetTriggerStatusForDailyCI failed")
		return err
	}

	return ctx.JSON(http.StatusOK, GetTriggerStatusForDailyCIResp{Finished: finished})
}

// GetDailyCIReport ...
// @Summary Returns JUnit report of executions.
// @ID get-dailyci-report
// @Tags dailyci
// @Produce	application/xml
// @Param id query integer true "id"
// @Param branch query string false "Filter by git branch" default(main)
// @Param plans query []string false "Filter by plans" collectionFormat(csv) maxItems(100) uniqueItems(true)
// @Success 200 {object} junit.JUnitTestSuites
// @Failure default {object} HTTPError
// @Router /dailyci [get]
func (h *HTTPHandler) GetDailyCIReport(c echo.Context) (err error) {
	ctx := c.(utils.Context)

	branch := ctx.QueryParam("branch")
	if branch == "" {
		branch = "main"
	}

	planNames, err := utils.ParseCSV(ctx.QueryParam("plans"))
	if err != nil {
		return serror.NewMsg(http.StatusBadRequest, "parse parameter plans failed: %v", err)
	}

	id_, err := strconv.ParseInt(ctx.QueryParam("id"), 10, 64)
	if err != nil {
		return serror.NewMsg(http.StatusBadRequest, "parse parameter id failed: %v", err)
	}
	id := time.Unix(id_, 0)

	// 1. Get plans
	plans, err := h.service.GitHubPlan.ListBranchPlans(ctx, branch, planNames)
	if err != nil {
		ctx.Log().Errorf(err, "dailyci: list plans failed")
		return err
	}
	planIDs := make([]model.ID, len(plans))
	for i, p := range plans {
		planIDs[i] = p.ID
	}

	// 2. Get executions of these plans
	execs, _, err := h.service.PlanExec.List(ctx, common.PlanExecQueryOpts{
		PlanIDs: planIDs,
		// filter out the executions that were created by user while debugging
		OnlyCron:  true,
		CreatedAt: &id,
		Count:     999,
	})
	if err != nil {
		ctx.Log().Errorf(err, "dailyci: list plan-execs failed")
		return err
	}

	// 3. Construct JUnit Suites
	ret, err := h.planExec2JUnitSuites(ctx, branch, execs)
	if err != nil {
		return err
	}
	return c.XML(http.StatusOK, ret)
}

func (h *HTTPHandler) planExec2JUnitSuites(ctx utils.Context, branch string, execs []*model.TestPlanExecution) (*junit.JUnitTestSuites, error) {
	suites := &junit.JUnitTestSuites{
		Suites: make([]junit.JUnitTestSuite, len(execs)),
	}
	for i, exec := range execs {
		// 4. Get case-execs of these plan-exec
		// TODO(gaozifeng): need parallel?
		opts := common.CaseExecQueryOpts{
			RootPlanExecID: (*model.ID)(&exec.ID),
			Offset:         uint(0),
			Count:          uint(999),
		}
		caseExecs, total, err := h.service.CaseExec.List(ctx, opts)
		if err != nil {
			ctx.Log().Errorf(err, "dailyci: list case-execs failed")
			return nil, err
		}

		// test cases
		failures := 0
		testCases := make([]junit.JUnitTestCase, len(caseExecs))
		for i, c := range caseExecs {
			// NOTE: isCancelOrSkip don't care about the status of the case-execs, just read the plan-exec's status
			isCancelOrSkip := exec.Status == model.ExecutionStatusCancelled || exec.Status == model.ExecutionStatusSkipped

			isFailed := !isCancelOrSkip && c.Status != model.ExecutionStatusSuccess
			if isFailed {
				failures++
			}
			duration := "0"
			if c.StartedAt != nil && c.CompletedAt != nil {
				dur := c.CompletedAt.Time().Sub(c.StartedAt.Time())
				duration = strconv.Itoa(int(dur.Seconds()))
			}
			var skipMessage *junit.JUnitSkipMessage
			if isCancelOrSkip {
				skipMessage = &junit.JUnitSkipMessage{
					Message: string(exec.Status),
				}
			}
			var failure *junit.JUnitFailure
			if c.Status != model.ExecutionStatusSuccess {
				url := fmt.Sprintf("https://tcms.pingcap.net/dashboard/executions/case/%d", c.ID)
				failure = &junit.JUnitFailure{
					Message:  url,
					Type:     string(c.Status),
					Contents: "",
				}
			}
			testCases[i] = junit.JUnitTestCase{
				Classname:   c.CaseIDAndName.ID.String(),
				Name:        c.CaseIDAndName.Name,
				Time:        duration,
				SkipMessage: skipMessage,
				Failure:     failure,
			}
		}

		// test suite
		duration := "0"
		startedAt := time.Time{}
		if exec.StartedAt != nil && exec.CompletedAt != nil {
			startedAt = exec.StartedAt.Time()
			dur := exec.CompletedAt.Time().Sub(startedAt)
			duration = strconv.Itoa(int(dur.Seconds()))
		}

		suite := junit.JUnitTestSuite{
			Tests:    total,
			Failures: failures,
			Time:     duration,
			Name:     exec.PlanIDAndName.Name,
			Properties: []junit.JUnitProperty{{
				Name:  "branch",
				Value: branch,
			}, {
				Name:  "startedAt",
				Value: strconv.FormatInt(startedAt.Unix(), 10),
			}, {
				Name:  "url",
				Value: fmt.Sprintf("https://tcms.pingcap.net/dashboard/executions/cases?root_plan_exec_id=%d", exec.ID),
			}},
			TestCases: testCases,
		}

		suites.Suites[i] = suite
	}
	return suites, nil
}
