// Copyright 2024 The etcd Authors
//
// 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,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cmd

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"
	"time"

	apipb "github.com/GoogleCloudPlatform/testgrid/pb/api/v1"
	statuspb "github.com/GoogleCloudPlatform/testgrid/pb/test_status"
	"google.golang.org/protobuf/encoding/protojson"
)

var (
	validTestStatuses      = []statuspb.TestStatus{statuspb.TestStatus_PASS, statuspb.TestStatus_FAIL, statuspb.TestStatus_FLAKY}
	failureTestStatuses    = []statuspb.TestStatus{statuspb.TestStatus_FAIL, statuspb.TestStatus_FLAKY}
	validTestStatusesInt   = intStatusSet(validTestStatuses)
	failureTestStatusesInt = intStatusSet(failureTestStatuses)

	skippedTestStatuses = make(map[int32]struct{})
)

type TestResultSummary struct {
	Name                  string
	FullName              string
	TotalRuns, FailedRuns int
	FailureRate           float32
	FailureLogs           []string
	IssueBody             string
}

func fetchTestResultSummaries(dashboard, tab string) []*TestResultSummary {
	// Fetch test data
	rowsURL := fmt.Sprintf("http://testgrid-data.k8s.io/api/v1/dashboards/%s/tabs/%s/rows", dashboard, tab)
	headersURL := fmt.Sprintf("http://testgrid-data.k8s.io/api/v1/dashboards/%s/tabs/%s/headers", dashboard, tab)

	var testData apipb.ListRowsResponse
	var headerData apipb.ListHeadersResponse
	protojson.Unmarshal(fetchJSON(rowsURL), &testData)
	protojson.Unmarshal(fetchJSON(headersURL), &headerData)

	var allTests []string
	for _, row := range testData.Rows {
		allTests = append(allTests, row.Name)
	}

	summaries := []*TestResultSummary{}
	// Process rows
	for _, row := range testData.Rows {
		t := processRow(dashboard, tab, row, allTests, headerData.Headers)
		summaries = append(summaries, t)
	}
	return summaries
}

func processRow(dashboard, tab string, row *apipb.ListRowsResponse_Row, allTests []string, headers []*apipb.ListHeadersResponse_Header) *TestResultSummary {
	t := TestResultSummary{Name: shortenTestName(row.Name), FullName: row.Name}
	// we do not want to create issues for a parent test.
	if isParentTest(row.Name, allTests) {
		return &t
	}
	if !strings.HasPrefix(row.Name, "go.etcd.io") {
		return &t
	}
	earliestTimeToConsider := time.Now().AddDate(0, 0, -1*maxDays)
	total := 0
	failed := 0
	logs := []string{}
	for i, cell := range row.Cells {
		// ignore tests with status not in the validTestStatuses
		// cell result codes are listed in https://github.com/GoogleCloudPlatform/testgrid/blob/main/pb/test_status/test_status.proto
		if _, ok := validTestStatusesInt[cell.Result]; !ok {
			if cell.Result != 0 {
				skippedTestStatuses[cell.Result] = struct{}{}
			}
			continue
		}
		header := headers[i]
		if maxDays > 0 && header.Started.AsTime().Before(earliestTimeToConsider) {
			continue
		}
		total++
		if _, ok := failureTestStatusesInt[cell.Result]; ok {
			failed++
			// markdown table format of | commit | log |
			logs = append(logs, fmt.Sprintf("| %s | %s | https://prow.k8s.io/view/gs/kubernetes-jenkins/logs/%s/%s |", strings.Join(header.Extra, ","), header.Started.AsTime().String(), tab, header.Build))
		}
		if maxRuns > 0 && total >= maxRuns {
			break
		}
	}
	t.FailedRuns = failed
	t.TotalRuns = total
	t.FailureLogs = logs
	t.FailureRate = float32(failed) / float32(total)
	if t.FailedRuns > 0 {
		dashboardURL := fmt.Sprintf("[%s](https://testgrid.k8s.io/%s#%s)", tab, dashboard, tab)
		t.IssueBody = fmt.Sprintf("## %s Test: %s \nTest failed %.1f%% (%d/%d) of the time\n\nfailure logs are:\n| commit | started | log |\n| --- | --- | --- |\n%s\n",
			dashboardURL, t.FullName, t.FailureRate*100, t.FailedRuns, t.TotalRuns, strings.Join(t.FailureLogs, "\n"))
		t.IssueBody += "\nPlease follow the [instructions in the contributing guide](https://github.com/etcd-io/etcd/blob/main/CONTRIBUTING.md#check-for-flaky-tests) to reproduce the issue.\n"
		fmt.Printf("%s failed %.1f%% (%d/%d) of the time\n", t.FullName, t.FailureRate*100, t.FailedRuns, t.TotalRuns)
	}
	return &t
}

// isParentTest checks if a test is a rollup of some child tests.
func isParentTest(test string, allTests []string) bool {
	for _, t := range allTests {
		if t != test && strings.HasPrefix(t, test+"/") {
			return true
		}
	}
	return false
}

func fetchJSON(url string) []byte {
	resp, err := http.Get(url)
	if err != nil {
		fmt.Println("Error fetching test data:", err)
		os.Exit(1)
	}
	defer resp.Body.Close()
	testBody, _ := io.ReadAll(resp.Body)
	return testBody
}

// intStatusSet converts a list of statuspb.TestStatus into a set of int.
func intStatusSet(statuses []statuspb.TestStatus) map[int32]struct{} {
	s := make(map[int32]struct{})
	for _, status := range statuses {
		s[int32(status)] = struct{}{}
	}
	return s
}

func shortenTestName(fullname string) string {
	parts := strings.Split(fullname, ".")
	return parts[len(parts)-1]
}
