package generator

import (
	"fmt"
	"log"

	"infra/cros/internal/match"

	"go.chromium.org/chromiumos/infra/proto/go/testplans"
)

type testType int

type testGroup string

const (
	hw testType = iota
	vm
)

var (
	testTypeFilter = map[testType]func(testReqs *testplans.SourceTestRules) bool{
		hw: func(testReqs *testplans.SourceTestRules) bool {
			return testReqs.GetSubtractiveRule().GetDisableHwTests()
		},
		vm: func(testReqs *testplans.SourceTestRules) bool {
			return testReqs.GetSubtractiveRule().GetDisableVmTests()
		},
	}
)

func (tt testType) String() string {
	return [...]string{"Hw", "Vm"}[tt]
}

type testPruneResult struct {
	disableHWTests                bool
	disableVMTests                bool
	onlyKeepAllSuitesInGroups     map[testGroup]bool
	onlyKeepOneSuiteFromEachGroup map[testGroup]bool
	addAllSuitesInGroups          map[testGroup]bool
	addOneSuiteFromEachGroup      map[testGroup]bool
}

func (tpr testPruneResult) hasOnlyKeepSuiteRules() bool {
	return len(tpr.onlyKeepOneSuiteFromEachGroup) > 0 || len(tpr.onlyKeepAllSuitesInGroups) > 0
}

func (tpr testPruneResult) hasAddAllOrOneTestRules() bool {
	return len(tpr.addOneSuiteFromEachGroup) > 0 || len(tpr.addAllSuitesInGroups) > 0
}

func (tpr testPruneResult) canSkipForOnlyTestRule(groups []*testplans.TestSuiteCommon_TestSuiteGroup) bool {

	if len(tpr.onlyKeepAllSuitesInGroups) == 0 {
		return false
	}
	for _, g := range groups {
		if tpr.onlyKeepAllSuitesInGroups[testGroup(g.TestSuiteGroup)] {
			return false
		}
	}
	return true
}

func (tpr testPruneResult) mustAddForAlsoTestRule(groups []*testplans.TestSuiteCommon_TestSuiteGroup) bool {
	for _, g := range groups {
		if tpr.addAllSuitesInGroups[testGroup(g.TestSuiteGroup)] {
			return true
		}
	}
	return false
}

func extractPruneResult(
	sourceTreeCfg *testplans.SourceTreeTestCfg,
	srcPaths []string) (*testPruneResult, error) {

	result := &testPruneResult{}

	if len(srcPaths) == 0 {

		log.Print("no gerrit_changes, so no tests will be skipped")
		return result, nil
	}

	disableHW := true
disableHWLoop:
	for _, fileSrcPath := range srcPaths {
		disableHWForPath, err := canDisableTestingForPath(fileSrcPath, sourceTreeCfg, hw)
		if err != nil {
			return result, err
		}
		if !disableHWForPath {
			log.Printf("cannot disable HW testing due to file %s", fileSrcPath)
			disableHW = false
			break disableHWLoop
		}
	}
	disableVM := true
disableVMLoop:
	for _, fileSrcPath := range srcPaths {
		disableVMForPath, err := canDisableTestingForPath(fileSrcPath, sourceTreeCfg, vm)
		if err != nil {
			return result, err
		}
		if !disableVMForPath {
			log.Printf("cannot disable VM testing due to file %s", fileSrcPath)
			disableVM = false
			break disableVMLoop
		}
	}

	onlyKeepAllSuitesInGroups := make(map[testGroup]bool)
	onlyKeepOneSuiteFromEachGroup := make(map[testGroup]bool)
fileLoop:
	for _, fileSrcPath := range srcPaths {
		fileOnlyTestGroups, err := getOnlyTestGroups(fileSrcPath, sourceTreeCfg)
		if err != nil {
			return result, err
		}
		fileOneofTestGroups, err := getOneofTestGroups(fileSrcPath, sourceTreeCfg)
		if err != nil {
			return result, err
		}
		if len(fileOnlyTestGroups) == 0 && len(fileOneofTestGroups) == 0 {
			log.Printf("cannot use subtractive test group rules on set of builders for testing due to %s", fileSrcPath)
			onlyKeepAllSuitesInGroups = make(map[testGroup]bool)
			onlyKeepOneSuiteFromEachGroup = make(map[testGroup]bool)
			break fileLoop
		} else {
			for g, include := range fileOnlyTestGroups {
				onlyKeepAllSuitesInGroups[g] = include
			}
			for g, include := range fileOneofTestGroups {
				onlyKeepOneSuiteFromEachGroup[g] = include
			}
		}

	}

	addAllSuitesInGroups := make(map[testGroup]bool)
	addOneSuiteFromEachGroup := make(map[testGroup]bool)
	for _, fileSrcPath := range srcPaths {
		fileAlsoTestGroups, err := getAddAllSuitesInGroups(fileSrcPath, sourceTreeCfg)
		if err != nil {
			return result, err
		}
		for k, v := range fileAlsoTestGroups {
			addAllSuitesInGroups[k] = v
			log.Printf("Will also test testGroup %v due to file %v", k, fileSrcPath)
		}

		fileAddOneSuiteFromEachGroup, err := getAddOneSuiteFromEachGroup(fileSrcPath, sourceTreeCfg)
		if err != nil {
			return result, err
		}
		for k, v := range fileAddOneSuiteFromEachGroup {
			addOneSuiteFromEachGroup[k] = v
			log.Printf("Will also test one suite from testGroup %v due to file %v", k, fileSrcPath)
		}
	}

	return &testPruneResult{
			disableHWTests:                disableHW,
			disableVMTests:                disableVM,
			onlyKeepAllSuitesInGroups:     onlyKeepAllSuitesInGroups,
			onlyKeepOneSuiteFromEachGroup: onlyKeepOneSuiteFromEachGroup,
			addAllSuitesInGroups:          addAllSuitesInGroups,
			addOneSuiteFromEachGroup:      addOneSuiteFromEachGroup},
		nil
}

func getOnlyTestGroups(
	sourcePath string,
	sourceTreeCfg *testplans.SourceTreeTestCfg) (map[testGroup]bool, error) {
	onlyTestGroups := make(map[testGroup]bool)
	for _, str := range sourceTreeCfg.GetSourceTestRules() {
		match, err := match.FilePatternMatches(str.GetFilePattern(), sourcePath)
		if err != nil {
			return onlyTestGroups, err
		}
		if match {
			okasig := str.GetSubtractiveRule().GetOnlyKeepAllSuitesInGroups()
			for _, o := range okasig.GetName() {
				onlyTestGroups[testGroup(o)] = true
			}
		}
	}
	return onlyTestGroups, nil
}

func getOneofTestGroups(
	sourcePath string,
	sourceTreeCfg *testplans.SourceTreeTestCfg) (map[testGroup]bool, error) {
	oneofTestGroups := make(map[testGroup]bool)
	for _, str := range sourceTreeCfg.GetSourceTestRules() {
		match, err := match.FilePatternMatches(str.GetFilePattern(), sourcePath)
		if err != nil {
			return oneofTestGroups, err
		}
		okosfeg := str.GetSubtractiveRule().GetOnlyKeepOneSuiteFromEachGroup()
		if match {
			for _, g := range okosfeg.GetName() {
				oneofTestGroups[testGroup(g)] = true
			}
		}
	}
	return oneofTestGroups, nil
}

func getAddAllSuitesInGroups(
	sourcePath string,
	sourceTreeCfg *testplans.SourceTreeTestCfg) (map[testGroup]bool, error) {
	alsoTestGroups := make(map[testGroup]bool)
	for _, str := range sourceTreeCfg.GetSourceTestRules() {
		match, err := match.FilePatternMatches(str.GetFilePattern(), sourcePath)
		if err != nil {
			return alsoTestGroups, err
		}
		if match {
			aasig := str.GetAdditiveRule().GetAddAllSuitesInGroups()
			for _, g := range aasig.GetName() {
				alsoTestGroups[testGroup(g)] = true
			}
		}
	}
	return alsoTestGroups, nil
}

func getAddOneSuiteFromEachGroup(
	sourcePath string,
	sourceTreeCfg *testplans.SourceTreeTestCfg) (map[testGroup]bool, error) {
	alsoTestOneofEachGroup := make(map[testGroup]bool)
	for _, str := range sourceTreeCfg.GetSourceTestRules() {
		match, err := match.FilePatternMatches(str.GetFilePattern(), sourcePath)
		if err != nil {
			return alsoTestOneofEachGroup, err
		}
		if match {
			aosfeg := str.GetAdditiveRule().GetAddOneSuiteFromEachGroup()
			for _, g := range aosfeg.GetName() {
				alsoTestOneofEachGroup[testGroup(g)] = true
			}
		}
	}
	return alsoTestOneofEachGroup, nil
}

func canDisableTestingForPath(sourcePath string, sourceTreeCfg *testplans.SourceTreeTestCfg, tt testType) (bool, error) {
	for _, str := range sourceTreeCfg.GetSourceTestRules() {
		testFilter, ok := testTypeFilter[tt]
		if !ok {
			return false, fmt.Errorf("Missing test filter for %v", tt)
		}
		if testFilter(str) {
			match, err := match.FilePatternMatches(str.GetFilePattern(), sourcePath)
			if err != nil {
				return false, err
			}
			if match {
				return true, nil
			}
		}
	}
	return false, nil
}
// Copyright 2021 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package generator

import (
	"fmt"
	"log"

	"infra/cros/internal/match"

	"go.chromium.org/chromiumos/infra/proto/go/testplans"
)

type testType int

type testGroup string

const (
	hw testType = iota
	vm
)

var (
	testTypeFilter = map[testType]func(testReqs *testplans.SourceTestRules) bool{
		hw: func(testReqs *testplans.SourceTestRules) bool {
			return testReqs.GetSubtractiveRule().GetDisableHwTests()
		},
		vm: func(testReqs *testplans.SourceTestRules) bool {
			return testReqs.GetSubtractiveRule().GetDisableVmTests()
		},
	}
)

func (tt testType) String() string {
	return [...]string{"Hw", "Vm"}[tt]
}

type testPruneResult struct {
	disableHWTests                bool
	disableVMTests                bool
	onlyKeepAllSuitesInGroups     map[testGroup]bool
	onlyKeepOneSuiteFromEachGroup map[testGroup]bool
	addAllSuitesInGroups          map[testGroup]bool
	addOneSuiteFromEachGroup      map[testGroup]bool
}

func (tpr testPruneResult) hasOnlyKeepSuiteRules() bool {
	return len(tpr.onlyKeepOneSuiteFromEachGroup) > 0 || len(tpr.onlyKeepAllSuitesInGroups) > 0
}

func (tpr testPruneResult) hasAddAllOrOneTestRules() bool {
	return len(tpr.addOneSuiteFromEachGroup) > 0 || len(tpr.addAllSuitesInGroups) > 0
}

func (tpr testPruneResult) canSkipForOnlyTestRule(groups []*testplans.TestSuiteCommon_TestSuiteGroup) bool {
	// If the source config didn't specify any onlyKeepAllSuitesInGroups, we can't skip testing for the groups in the params.
	if len(tpr.onlyKeepAllSuitesInGroups) == 0 {
		return false
	}
	for _, g := range groups {
		if tpr.onlyKeepAllSuitesInGroups[testGroup(g.TestSuiteGroup)] {
			return false
		}
	}
	return true
}

func (tpr testPruneResult) mustAddForAlsoTestRule(groups []*testplans.TestSuiteCommon_TestSuiteGroup) bool {
	for _, g := range groups {
		if tpr.addAllSuitesInGroups[testGroup(g.TestSuiteGroup)] {
			return true
		}
	}
	return false
}

func extractPruneResult(
	sourceTreeCfg *testplans.SourceTreeTestCfg,
	srcPaths []string) (*testPruneResult, error) {

	result := &testPruneResult{}

	if len(srcPaths) == 0 {
		// This happens during postsubmit runs, for example.
		log.Print("no gerrit_changes, so no tests will be skipped")
		return result, nil
	}

	disableHW := true
disableHWLoop:
	for _, fileSrcPath := range srcPaths {
		disableHWForPath, err := canDisableTestingForPath(fileSrcPath, sourceTreeCfg, hw)
		if err != nil {
			return result, err
		}
		if !disableHWForPath {
			log.Printf("cannot disable HW testing due to file %s", fileSrcPath)
			disableHW = false
			break disableHWLoop
		}
	}
	disableVM := true
disableVMLoop:
	for _, fileSrcPath := range srcPaths {
		disableVMForPath, err := canDisableTestingForPath(fileSrcPath, sourceTreeCfg, vm)
		if err != nil {
			return result, err
		}
		if !disableVMForPath {
			log.Printf("cannot disable VM testing due to file %s", fileSrcPath)
			disableVM = false
			break disableVMLoop
		}
	}

	onlyKeepAllSuitesInGroups := make(map[testGroup]bool)
	onlyKeepOneSuiteFromEachGroup := make(map[testGroup]bool)
fileLoop:
	for _, fileSrcPath := range srcPaths {
		fileOnlyTestGroups, err := getOnlyTestGroups(fileSrcPath, sourceTreeCfg)
		if err != nil {
			return result, err
		}
		fileOneofTestGroups, err := getOneofTestGroups(fileSrcPath, sourceTreeCfg)
		if err != nil {
			return result, err
		}
		if len(fileOnlyTestGroups) == 0 && len(fileOneofTestGroups) == 0 {
			log.Printf("cannot use subtractive test group rules on set of builders for testing due to %s", fileSrcPath)
			onlyKeepAllSuitesInGroups = make(map[testGroup]bool)
			onlyKeepOneSuiteFromEachGroup = make(map[testGroup]bool)
			break fileLoop
		} else {
			for g, include := range fileOnlyTestGroups {
				onlyKeepAllSuitesInGroups[g] = include
			}
			for g, include := range fileOneofTestGroups {
				onlyKeepOneSuiteFromEachGroup[g] = include
			}
		}

	}

	addAllSuitesInGroups := make(map[testGroup]bool)
	addOneSuiteFromEachGroup := make(map[testGroup]bool)
	for _, fileSrcPath := range srcPaths {
		fileAlsoTestGroups, err := getAddAllSuitesInGroups(fileSrcPath, sourceTreeCfg)
		if err != nil {
			return result, err
		}
		for k, v := range fileAlsoTestGroups {
			addAllSuitesInGroups[k] = v
			log.Printf("Will also test testGroup %v due to file %v", k, fileSrcPath)
		}

		fileAddOneSuiteFromEachGroup, err := getAddOneSuiteFromEachGroup(fileSrcPath, sourceTreeCfg)
		if err != nil {
			return result, err
		}
		for k, v := range fileAddOneSuiteFromEachGroup {
			addOneSuiteFromEachGroup[k] = v
			log.Printf("Will also test one suite from testGroup %v due to file %v", k, fileSrcPath)
		}
	}

	return &testPruneResult{
			disableHWTests:                disableHW,
			disableVMTests:                disableVM,
			onlyKeepAllSuitesInGroups:     onlyKeepAllSuitesInGroups,
			onlyKeepOneSuiteFromEachGroup: onlyKeepOneSuiteFromEachGroup,
			addAllSuitesInGroups:          addAllSuitesInGroups,
			addOneSuiteFromEachGroup:      addOneSuiteFromEachGroup},
		nil
}

func getOnlyTestGroups(
	sourcePath string,
	sourceTreeCfg *testplans.SourceTreeTestCfg) (map[testGroup]bool, error) {
	onlyTestGroups := make(map[testGroup]bool)
	for _, str := range sourceTreeCfg.GetSourceTestRules() {
		match, err := match.FilePatternMatches(str.GetFilePattern(), sourcePath)
		if err != nil {
			return onlyTestGroups, err
		}
		if match {
			okasig := str.GetSubtractiveRule().GetOnlyKeepAllSuitesInGroups()
			for _, o := range okasig.GetName() {
				onlyTestGroups[testGroup(o)] = true
			}
		}
	}
	return onlyTestGroups, nil
}

// getOneofTestGroups extracts rules from config about any type of oneof testing
// that can be done for the provided path. For each of the keys in the returned
// map, at least one test suite must be tested.
func getOneofTestGroups(
	sourcePath string,
	sourceTreeCfg *testplans.SourceTreeTestCfg) (map[testGroup]bool, error) {
	oneofTestGroups := make(map[testGroup]bool)
	for _, str := range sourceTreeCfg.GetSourceTestRules() {
		match, err := match.FilePatternMatches(str.GetFilePattern(), sourcePath)
		if err != nil {
			return oneofTestGroups, err
		}
		okosfeg := str.GetSubtractiveRule().GetOnlyKeepOneSuiteFromEachGroup()
		if match {
			for _, g := range okosfeg.GetName() {
				oneofTestGroups[testGroup(g)] = true
			}
		}
	}
	return oneofTestGroups, nil
}

func getAddAllSuitesInGroups(
	sourcePath string,
	sourceTreeCfg *testplans.SourceTreeTestCfg) (map[testGroup]bool, error) {
	alsoTestGroups := make(map[testGroup]bool)
	for _, str := range sourceTreeCfg.GetSourceTestRules() {
		match, err := match.FilePatternMatches(str.GetFilePattern(), sourcePath)
		if err != nil {
			return alsoTestGroups, err
		}
		if match {
			aasig := str.GetAdditiveRule().GetAddAllSuitesInGroups()
			for _, g := range aasig.GetName() {
				alsoTestGroups[testGroup(g)] = true
			}
		}
	}
	return alsoTestGroups, nil
}

func getAddOneSuiteFromEachGroup(
	sourcePath string,
	sourceTreeCfg *testplans.SourceTreeTestCfg) (map[testGroup]bool, error) {
	alsoTestOneofEachGroup := make(map[testGroup]bool)
	for _, str := range sourceTreeCfg.GetSourceTestRules() {
		match, err := match.FilePatternMatches(str.GetFilePattern(), sourcePath)
		if err != nil {
			return alsoTestOneofEachGroup, err
		}
		if match {
			aosfeg := str.GetAdditiveRule().GetAddOneSuiteFromEachGroup()
			for _, g := range aosfeg.GetName() {
				alsoTestOneofEachGroup[testGroup(g)] = true
			}
		}
	}
	return alsoTestOneofEachGroup, nil
}

// canDisableTestingForPath determines whether a particular testing type is unnecessary for
// a given file, based on source tree test restrictions.
func canDisableTestingForPath(sourcePath string, sourceTreeCfg *testplans.SourceTreeTestCfg, tt testType) (bool, error) {
	for _, str := range sourceTreeCfg.GetSourceTestRules() {
		testFilter, ok := testTypeFilter[tt]
		if !ok {
			return false, fmt.Errorf("Missing test filter for %v", tt)
		}
		if testFilter(str) {
			match, err := match.FilePatternMatches(str.GetFilePattern(), sourcePath)
			if err != nil {
				return false, err
			}
			if match {
				return true, nil
			}
		}
	}
	return false, nil
}
