// Copyright (c) 2026 Tigera, Inc. All rights reserved.
//
// 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 cilanes

import (
	"os"
	"testing"

	. "github.com/onsi/gomega"
)

const argoFixture = `
globalPrologue: |
  export E2E_TEST_CONFIG="${E2E_TEST_CONFIG:-e2e/config/iptables/xtables.yaml}"
  export INSTALLER="${INSTALLER:-operator}"
  export FUNCTIONAL_AREA="${FUNCTIONAL_AREA:-iptables.yml}"
  export TEST_TYPE="${OTHER_TYPE:-nonsense}"
steps:
  - name: inherits-default
    commands: |
      .argoci/scripts/body_standard.sh
  - name: overrides-config
    env:
      - name: E2E_TEST_CONFIG
        value: e2e/config/vpp/eks.yaml
    commands: |
      .argoci/scripts/body_standard.sh
  - name: config-list
    env:
      - name: E2E_TEST_CONFIG
        value: >-
          e2e/config/ci-base.yaml
          e2e/config/suite/policy.yaml
    commands: |
      .argoci/scripts/body_standard.sh
  - name: openstack
    env:
      - name: PROVISIONER
        value: gcp-openstack
    commands: |
      .argoci/scripts/body_standard.sh
  - name: migration
    commands: |
      .argoci/scripts/body_flannel-migration.sh
  - name: matrix-step
    matrix:
      - name: calico-cni
        env:
          - name: CNI
            value: calico
      - name: encap
        env:
          - name: E2E_TEST_CONFIG
            value: e2e/config/iptables/xtables-encap-nobgp.yaml
    commands: |
      .argoci/scripts/body_standard.sh
`

func TestParseArgo(t *testing.T) {
	RegisterTestingT(t)

	lanes, err := parseArgo("cron.yaml", []byte(argoFixture))
	Expect(err).NotTo(HaveOccurred())

	byName := map[string]Lane{}
	for _, l := range lanes {
		byName[l.Name] = l
	}

	inherited := byName["inherits-default"]
	Expect(inherited.Config).To(Equal("e2e/config/iptables/xtables.yaml"))
	Expect(inherited.TestType).To(Equal("k8s-e2e"))
	Expect(inherited.Area).To(Equal("iptables.yml"))
	Expect(inherited.RunsE2EBinary()).To(BeTrue())

	// The prologue's TEST_TYPE defaults off a different variable, so it does not
	// resolve to a value the parser can use.
	Expect(byName["overrides-config"].Config).To(Equal("e2e/config/vpp/eks.yaml"))

	// A cell can list several configs, folded onto one line by the YAML.
	Expect(byName["config-list"].Configs()).To(Equal([]string{"e2e/config/ci-base.yaml", "e2e/config/suite/policy.yaml"}))
	Expect(byName["overrides-config"].Configs()).To(Equal([]string{"e2e/config/vpp/eks.yaml"}))

	openstack := byName["openstack"]
	Expect(openstack.TestType).To(Equal("openstack-e2e"))
	Expect(openstack.RunsE2EBinary()).To(BeFalse())

	// The migration job runs the suite twice, so it resolves to two lanes.
	Expect(byName["migration"].Config).To(Equal("e2e/config/iptables/xtables.yaml"))
	Expect(byName["migration [pre-migration]"].Config).To(Equal(flannelMigrationPreConfig))

	// Every matrix entry is its own lane, including one that selects like the
	// step's default, so a lane keeps its name when an entry's config changes.
	Expect(byName).NotTo(HaveKey("matrix-step"))
	Expect(byName["matrix-step [calico-cni]"].Config).To(Equal("e2e/config/iptables/xtables.yaml"))
	Expect(byName["matrix-step [encap]"].Config).To(Equal("e2e/config/iptables/xtables-encap-nobgp.yaml"))
}

const semaphoreSelectionMatrix = `
- name: Block one
  task:
    jobs:
      - name: matrixed
        matrix:
          - env_var: E2E_TEST_CONFIG
            values: ["e2e/config/nftables/xtables.yaml"]
        commands:
          - body_standard.sh
`

func TestParseSemaphoreBlocksRefusesSelectionMatrix(t *testing.T) {
	RegisterTestingT(t)

	// No block varies the selection by matrix, so the parser refuses to guess.
	_, err := parseSemaphoreBlocks("blocks/20-e2e.yml", []byte(semaphoreSelectionMatrix))
	Expect(err).To(MatchError(ContainSubstring("matrix on E2E_TEST_CONFIG")))
}

func TestSelectionArgs(t *testing.T) {
	RegisterTestingT(t)

	args, err := Lane{Config: "e2e/config/vpp/eks.yaml"}.SelectionArgs("/repo")
	Expect(err).NotTo(HaveOccurred())
	Expect(args).To(Equal([]string{"--calico.test-config=/repo/e2e/config/vpp/eks.yaml"}))

	// A lane listing several configs passes each, in order. An entry is relative
	// to e2e/config unless it already starts with e2e/config/, as in e2e-run.
	args, err = Lane{Name: "list", Config: "ci-base.yaml\n  e2e/config/suite/policy.yaml"}.SelectionArgs("/repo")
	Expect(err).NotTo(HaveOccurred())
	Expect(args).To(Equal([]string{
		"--calico.test-config=/repo/e2e/config/ci-base.yaml",
		"--calico.test-config=/repo/e2e/config/suite/policy.yaml",
	}))

	// An entry that can't name a file under e2e/config is caught here rather
	// than surfacing as an opaque binary failure.
	for _, bad := range []string{"/tmp/x.yaml", "${CFG}", "../x.yaml", "e2e/config/../../x.yaml"} {
		_, err = Lane{Name: "bad", Config: bad}.SelectionArgs("/repo")
		Expect(err).To(MatchError(ContainSubstring("is not under e2e/config/")), bad)
	}

	_, err = Lane{Name: "empty"}.SelectionArgs("/repo")
	Expect(err).To(MatchError(ContainSubstring("selects nothing")))

	// Whitespace alone is no selection, so the lane does not run the binary.
	Expect(Lane{TestType: "k8s-e2e", Config: "  "}.RunsE2EBinary()).To(BeFalse())
	Expect(Lane{TestType: "k8s-e2e", Config: "e2e/config/a.yaml"}.RunsE2EBinary()).To(BeTrue())
}

const blocksFixture = `
- name: E2E tests (KinD)
  task:
    jobs:
      - name: Conformance
        commands:
          - .semaphore/run-and-monitor e2e-test.log make e2e-test
      - name: Felix routing
        env_vars:
          - name: E2E_TEST_CONFIG
            value: e2e/config/kind/felix-routing.yaml
        commands:
          - .semaphore/run-and-monitor e2e-test-felix.log make e2e-test
      - name: ClusterNetworkPolicy
        env_vars:
          - name: E2E_TEST_CONFIG
            value: e2e/config/kind/felix-routing.yaml
        commands:
          - make e2e-test-clusternetworkpolicy
      - name: BPF
        env_vars:
          - name: E2E_TEST_CONFIG
            value: e2e/config/kind/felix-routing.yaml
        commands:
          - make e2e-test-bpf
- name: KubeVirt live migration (KIND)
  task:
    env_vars:
      - name: E2E_TEST_CONFIG
        value: e2e/config/kind/kubevirt.yaml
    jobs:
      - name: KubeVirt live migration (KIND)
        commands:
          - make kind-up
          - make e2e-run KUBECONFIG=kubeconfig
- name: Felix
  task:
    jobs:
      - name: Felix UT
        commands:
          - make -C felix ut
- name: E2E tests on GCP kubeadm
  task:
    env_vars:
      - name: E2E_TEST_CONFIG
        value: e2e/config/iptables/xtables.yaml
    jobs:
      - name: gcp-kubeadm
        commands:
          - .argoci/scripts/body_standard.sh
      - name: matrixed
        matrix:
          - env_var: NETWORK_PLUGIN
            values: ["calico", "aws"]
        commands:
          - .argoci/scripts/body_standard.sh
`

func TestParseSemaphoreBlocks(t *testing.T) {
	RegisterTestingT(t)

	lanes, err := parseSemaphoreBlocks("blocks/20-e2e.yml", []byte(blocksFixture))
	Expect(err).NotTo(HaveOccurred())

	byName := map[string]Lane{}
	for _, l := range lanes {
		byName[l.Name] = l
	}

	// No config in the environment, so the target's Makefile default applies.
	Expect(byName["E2E tests (KinD) / Conformance"].Config).To(Equal("e2e/config/kind/conformance.yaml"))
	Expect(byName["E2E tests (KinD) / Conformance"].RunsE2EBinary()).To(BeTrue())

	// A config in the environment beats that default.
	Expect(byName["E2E tests (KinD) / Felix routing"].Config).To(Equal("e2e/config/kind/felix-routing.yaml"))

	// e2e-test-bpf hardcodes its config, so the environment's is ignored.
	Expect(byName["E2E tests (KinD) / BPF"].Config).To(Equal("e2e/config/kind/bpf.yaml"))

	// The ClusterNetworkPolicy job runs a different binary that takes no config,
	// so it is not a lane even though it sets one.
	Expect(byName).NotTo(HaveKey("E2E tests (KinD) / ClusterNetworkPolicy"))

	// A block whose job runs the suite through e2e-run keeps the environment's.
	kubevirt := byName["KubeVirt live migration (KIND) / KubeVirt live migration (KIND)"]
	Expect(kubevirt.Config).To(Equal("e2e/config/kind/kubevirt.yaml"))

	// Component blocks declare no e2e lane.
	Expect(byName).NotTo(HaveKey("Felix / Felix UT"))

	// A provisioned lane in the same directory still reads its config from the
	// environment.
	Expect(byName["E2E tests on GCP kubeadm / gcp-kubeadm"].Config).To(Equal("e2e/config/iptables/xtables.yaml"))

	// A non-selection axis leaves the job as one lane, unsuffixed.
	Expect(byName["E2E tests on GCP kubeadm / matrixed"].Config).To(Equal("e2e/config/iptables/xtables.yaml"))
}

// kindTargets duplicates what the Makefile does with E2E_TEST_CONFIG, so a
// change to either has to be a change to both.
func TestKindTargetsMatchTheMakefile(t *testing.T) {
	RegisterTestingT(t)

	makefile, err := os.ReadFile("../../../Makefile")
	Expect(err).NotTo(HaveOccurred())
	text := string(makefile)

	for target, want := range kindTargets {
		Expect(text).To(ContainSubstring("\n"+target+":"), "target %s is gone from the Makefile", target)
		if want.fixed != "" {
			Expect(text).To(ContainSubstring("E2E_TEST_CONFIG=$(REPO_ROOT)/"+want.fixed),
				"target %s no longer hardcodes %s", target, want.fixed)
		}
		if want.fallback != "" {
			Expect(text).To(ContainSubstring("E2E_TEST_CONFIG ?= "+want.fallback),
				"the Makefile default is no longer %s", want.fallback)
		}
	}
}

const moduleFixture = `
version: condensed-module
steps:
  - name: lints-things
    commands: |
      make static-checks
  - name: runs-the-suite
    env:
      - name: E2E_TEST_CONFIG
        value: e2e/config/gcp/bpf.yaml
    commands: |
      .argoci/scripts/body_standard.sh
`

// Only a module's spec-selecting steps are lanes.
func TestParseArgoModuleKeepsOnlyLanesThatSelectSpecs(t *testing.T) {
	RegisterTestingT(t)

	lanes, err := parseArgoModule("20-e2e-gcp.yaml", []byte(moduleFixture))
	Expect(err).NotTo(HaveOccurred())

	Expect(lanes).To(HaveLen(1))
	Expect(lanes[0].Name).To(Equal("runs-the-suite"))
	Expect(lanes[0].Config).To(Equal("e2e/config/gcp/bpf.yaml"))
}

const kindModuleFixture = `
version: condensed-module
steps:
  - name: e2e-kind
    matrix:
      - name: defaulted
        env:
          - name: E2E_TARGET
            value: e2e-test
      - name: from-env
        env:
          - name: E2E_TARGET
            value: e2e-test
          - name: E2E_TEST_CONFIG
            value: e2e/config/kind/felix-routing.yaml
      - name: other-binary
        env:
          - name: E2E_TARGET
            value: e2e-test-clusternetworkpolicy
          - name: E2E_TEST_CONFIG
            value: e2e/config/kind/felix-routing.yaml
      - name: hardcoded
        env:
          - name: E2E_TARGET
            value: e2e-test-bpf
    commands: |
      make ${E2E_TARGET}
  - name: manifest-install
    commands: |
      make kind-manifest-install-test
`

// A kind step's make target picks its config, even when a matrix entry names
// the target, and a step running some other binary is no lane.
func TestParseArgoModuleResolvesKindTargets(t *testing.T) {
	RegisterTestingT(t)

	lanes, err := parseArgoModule("20-e2e.yaml", []byte(kindModuleFixture))
	Expect(err).NotTo(HaveOccurred())

	configs := map[string]string{}
	for _, l := range lanes {
		configs[l.Name] = l.Config
	}
	Expect(configs).To(Equal(map[string]string{
		"e2e-kind [defaulted]": "e2e/config/kind/conformance.yaml",
		"e2e-kind [from-env]":  "e2e/config/kind/felix-routing.yaml",
		"e2e-kind [hardcoded]": "e2e/config/kind/bpf.yaml",
		"manifest-install":     "e2e/config/kind/manifest-install.yaml",
	}))
}
