package main

import (
	"flag"
	"fmt"
	"os"
	"strings"
	"testing"

	"github.com/k3s-io/k3s/tests"
	"github.com/k3s-io/k3s/tests/docker"
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

var k3sImage = flag.String("k3sImage", "", "The image used to provision containers")
var ci = flag.Bool("ci", false, "running on CI, forced cleanup")
var config *docker.TestConfig

func Test_DockerBasic(t *testing.T) {
	flag.Parse()
	RegisterFailHandler(Fail)
	RunSpecs(t, "Basic Docker Test Suite")
}

var _ = Describe("Basic Tests", Ordered, func() {

	Context("Setup Cluster", func() {
		It("should provision servers and agents", func() {
			var err error
			config, err = docker.NewTestConfig(*k3sImage)
			Expect(err).NotTo(HaveOccurred())
			Expect(config.ProvisionServers(1)).To(Succeed())
			Expect(config.ProvisionAgents(1)).To(Succeed())
			Eventually(func() error {
				return tests.CheckDefaultDeployments(config.KubeconfigFile)
			}, "180s", "5s").Should(Succeed())
			Eventually(func() error {
				return tests.NodesReady(config.KubeconfigFile, config.GetNodeNames())
			}, "40s", "5s").Should(Succeed())
		})
	})

	Context("Use Local Storage Volume", func() {
		It("should apply local storage volume", func() {
			_, err := config.DeployWorkload("volume-test.yaml")
			Expect(err).NotTo(HaveOccurred(), "failed to apply volume test manifest")
		})
		It("should validate local storage volume", func() {
			Eventually(func() (bool, error) {
				return tests.PodReady("volume-test", "kube-system", config.KubeconfigFile)
			}, "20s", "5s").Should(BeTrue())
		})
	})

	Context("Verify Binaries and Images", func() {
		It("has valid bundled binaries", func() {
			for _, server := range config.Servers {
				Expect(docker.VerifyValidVersion(server, "kubectl")).To(Succeed())
				Expect(docker.VerifyValidVersion(server, "ctr")).To(Succeed())
				Expect(docker.VerifyValidVersion(server, "crictl")).To(Succeed())
			}
		})
		It("has valid airgap images", func() {
			Expect(config).To(Not(BeNil()))
			err := VerifyAirgapImages(config)
			Expect(err).NotTo(HaveOccurred())
		})
	})
})

var failed bool
var _ = AfterEach(func() {
	failed = failed || CurrentSpecReport().Failed()
})

var _ = AfterSuite(func() {
	if failed {
		AddReportEntry("describe", docker.DescribeNodesAndPods(config))
		AddReportEntry("docker-containers", docker.ListContainers())
		AddReportEntry("docker-logs", docker.TailDockerLogs(1000, append(config.Servers, config.Agents...)))
	}
	if config != nil && (*ci || !failed) {
		Expect(config.Cleanup()).To(Succeed())
	}
})

// VerifyAirgapImages checks for changes in the airgap image list
func VerifyAirgapImages(config *docker.TestConfig) error {
	// This file is generated during the build packaging step
	const airgapImageList = "../../../scripts/airgap/image-list.txt"

	// Use a map to automatically handle duplicates
	imageSet := make(map[string]struct{})

	// Collect all images from nodes
	for _, node := range config.GetNodeNames() {
		cmd := fmt.Sprintf("docker exec %s crictl images -o json | jq -r '.images[].repoTags[0] | select(. != null)'", node)
		output, err := tests.RunCommand(cmd)
		Expect(err).NotTo(HaveOccurred(), "failed to execute crictl and jq: %v", err)

		for _, line := range strings.Split(strings.TrimSpace(string(output)), "\n") {
			if line != "" {
				imageSet[line] = struct{}{}
			}
		}
	}

	// Convert map keys to slice
	uniqueImages := make([]string, 0, len(imageSet))
	for image := range imageSet {
		uniqueImages = append(uniqueImages, image)
	}

	existing, err := os.ReadFile(airgapImageList)
	if err != nil && !os.IsNotExist(err) {
		return fmt.Errorf("failed to read airgap list file: %v", err)
	}

	// Sorting doesn't matter with ConsistOf
	existingImages := strings.Split(strings.TrimSpace(string(existing)), "\n")
	Expect(existingImages).To(ConsistOf(uniqueImages))
	return nil
}
