package grype_test

import (
	"archive/tar"
	"compress/gzip"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"os/exec"
	"strings"
	"testing"

	"github.com/ckotzbauer/libk8soci/pkg/oci"
	"github.com/ckotzbauer/vulnerability-operator/internal/vuln"
	"github.com/ckotzbauer/vulnerability-operator/internal/vuln/grype"
	"github.com/ckotzbauer/vulnerability-operator/internal/vuln/kubernetes"
	"github.com/ckotzbauer/vulnerability-operator/internal/vuln/source"
	"github.com/stretchr/testify/assert"
	"golang.org/x/mod/modfile"
)

type testData struct {
	testType string
	imageID  string
}

type vulnerabilitySlim struct {
	id       string
	name     string
	group    string
	severity string
}

type VulnerabilityRaw struct {
	Matches []struct {
		Vulnerability struct {
			ID       string `json:"id"`
			Severity string `json:"severity"`
		} `json:"vulnerability"`
		Artifact struct {
			Name   string `json:"name"`
			Origin string `json:"type"`
		} `json:"artifact"`
	} `json:"matches"`
}

func loadExpectedVulns(t *testing.T, testType, imageID string, image bool) []vulnerabilitySlim {
	downloadGrype(t)
	arg := ""
	if image {
		arg = fmt.Sprintf("registry:%s", imageID)
	} else {
		arg = fmt.Sprintf("sbom:fixtures/%s-alpine-sbom.json", testType)
	}

	cmd := exec.Command("./grype_amd64", arg, "-o", "json", "--file", fmt.Sprintf("%s.json", testType))
	_, err := cmd.Output()
	assert.NoError(t, err)

	data, err := os.ReadFile(fmt.Sprintf("%s.json", testType))
	assert.NoError(t, err)
	err = os.Remove(fmt.Sprintf("%s.json", testType))
	assert.NoError(t, err)

	parsed := VulnerabilityRaw{}
	err = json.Unmarshal(data, &parsed)
	assert.NoError(t, err)
	return convertRawData(parsed)
}

func testSbomVulnerabilities(t *testing.T, testType, imageID string) {
	expected := loadExpectedVulns(t, testType, imageID, false)

	g, err := grype.New("", "", "medium", false, true)
	assert.NoError(t, err)
	g = *g.WithTestContainer(&kubernetes.ContainerInfo{})

	b, _ := os.ReadFile(fmt.Sprintf("fixtures/%s-alpine-sbom.json", testType))
	vulnerabilities, err := g.ScanItem(source.Sbom{Sbom: string(b), ImageID: imageID})
	assert.NoError(t, err)
	vulnSlim := convertVulns(vulnerabilities.FoundVulnerabilities)

	assert.ElementsMatch(t, expected, vulnSlim)
}

func testImageVulnerabilities(t *testing.T, testType, imageID string) {
	expected := loadExpectedVulns(t, testType, imageID, true)

	g, err := grype.New("", "", "medium", false, true)
	assert.NoError(t, err)
	g = *g.WithTestContainer(&kubernetes.ContainerInfo{})

	vulnerabilities, err := g.ScanItem(source.Image{RegistryImage: oci.RegistryImage{ImageID: imageID}})
	assert.NoError(t, err)
	vulnSlim := convertVulns(vulnerabilities.FoundVulnerabilities)

	assert.ElementsMatch(t, expected, vulnSlim)
}

func convertVulns(vulnerabilities []vuln.Vulnerability) []vulnerabilitySlim {
	vulnSlim := make([]vulnerabilitySlim, 0)
	for _, v := range vulnerabilities {
		vulnSlim = append(vulnSlim, vulnerabilitySlim{
			id:       v.ID,
			name:     v.Package,
			group:    v.Type,
			severity: v.Severity,
		})
	}

	return vulnSlim
}

func convertRawData(rawData VulnerabilityRaw) []vulnerabilitySlim {
	vulnSlim := make([]vulnerabilitySlim, 0)
	for _, v := range rawData.Matches {
		if v.Vulnerability.Severity == "Unknown" {
			continue
		}

		vulnSlim = append(vulnSlim, vulnerabilitySlim{
			id:       v.Vulnerability.ID,
			name:     v.Artifact.Name,
			group:    v.Artifact.Origin,
			severity: v.Vulnerability.Severity,
		})
	}

	return vulnSlim
}

func downloadGrype(t assert.TestingT) {
	if _, err := os.Stat("grype"); err == nil {
		return
	}

	data, _ := os.ReadFile("../../../go.mod")
	f, _ := modfile.Parse("go.mod", data, nil)
	version := ""
	for _, req := range f.Require {
		if req.Mod.Path == "github.com/anchore/grype" {
			version = req.Mod.Version
			break
		}
	}

	url := fmt.Sprintf("https://github.com/anchore/grype/releases/download/%s/grype_%s_linux_amd64.tar.gz", version, strings.Replace(version, "v", "", 1))
	response, err := http.Get(url)
	assert.NoError(t, err)
	// nolint
	defer response.Body.Close()
	extractTarGz(t, response.Body)
}

func extractTarGz(t assert.TestingT, gzipStream io.Reader) {
	uncompressedStream, err := gzip.NewReader(gzipStream)
	assert.NoError(t, err)
	tarReader := tar.NewReader(uncompressedStream)

	for {
		header, err := tarReader.Next()
		if err == io.EOF {
			break
		}

		assert.NoError(t, err)
		if header.Name != "grype" {
			continue
		}

		outFile, err := os.Create(fmt.Sprintf("%s_amd64", header.Name))
		assert.NoError(t, err)

		if _, err := io.Copy(outFile, tarReader); err != nil {
			assert.NoError(t, err)
		}

		err = outFile.Chmod(0755)
		assert.NoError(t, err)

		err = outFile.Close()
		assert.NoError(t, err)
		break
	}
}

func TestGrypeSbom(t *testing.T) {
	tests := []testData{
		{
			testType: "vulnerable",
			imageID:  "alpine:3.17.0",
		},
		{
			testType: "clean",
			imageID:  "alpine:3.17.3",
		},
	}

	for _, v := range tests {
		t.Run(fmt.Sprintf("test-sbom-%s", v.testType), func(t *testing.T) {
			testSbomVulnerabilities(t, v.testType, v.imageID)
		})
	}
}

func TestGrypeImage(t *testing.T) {
	tests := []testData{
		{
			testType: "vulnerable",
			imageID:  "alpine:3.17.0",
		},
		{
			testType: "clean",
			imageID:  "alpine:3.17.3",
		},
	}

	for _, v := range tests {
		t.Run(fmt.Sprintf("test-image-%s", v.testType), func(t *testing.T) {
			testImageVulnerabilities(t, v.testType, v.imageID)
		})
	}
}
