package v6

import (
	"strings"
	"testing"
	"time"
	"unicode"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/anchore/grype/grype/version"
	"github.com/anchore/grype/grype/vulnerability"
)

func TestV5Namespace(t *testing.T) {
	// provider input should be derived from the Providers table:
	// +------------+---------+---------------+----------------------------------+------------------------+
	// | id         | version | processor     | date_captured                    | input_digest           |
	// +------------+---------+---------------+----------------------------------+------------------------+
	// | nvd        | 2       | vunnel@0.29.0 | 2025-01-08 01:32:55.179881+00:00 | xxh64:0a160d2b53dd0208 |
	// | alpine     | 1       | vunnel@0.29.0 | 2025-01-08 01:31:28.824872+00:00 | xxh64:30c5b7b8efa0c087 |
	// | amazon     | 1       | vunnel@0.29.0 | 2025-01-08 01:31:28.837469+00:00 | xxh64:7d90b3fa66b183bc |
	// | chainguard | 1       | vunnel@0.29.0 | 2025-01-08 01:31:26.969865+00:00 | xxh64:25a82fa97ac9e077 |
	// | debian     | 1       | vunnel@0.29.0 | 2025-01-08 01:31:50.718966+00:00 | xxh64:4b1834b9e4e68987 |
	// | github     | 1       | vunnel@0.29.0 | 2025-01-08 01:31:27.450124+00:00 | xxh64:a3ee6b48d37a0124 |
	// | mariner    | 1       | vunnel@0.29.0 | 2025-01-08 01:32:35.005761+00:00 | xxh64:cb4f5861a1fda0af |
	// | oracle     | 1       | vunnel@0.29.0 | 2025-01-08 01:32:33.696274+00:00 | xxh64:72c0a15731e96ab3 |
	// | rhel       | 1       | vunnel@0.29.0 | 2025-01-08 01:32:32.192345+00:00 | xxh64:abf5d2fd5a26c194 |
	// | sles       | 1       | vunnel@0.29.0 | 2025-01-08 01:32:42.988937+00:00 | xxh64:8f558f8f28a04489 |
	// | ubuntu     | 3       | vunnel@0.29.0 | 2025-01-08 01:33:25.795537+00:00 | xxh64:97ef8421c0093620 |
	// | wolfi      | 1       | vunnel@0.29.0 | 2025-01-08 01:32:58.571417+00:00 | xxh64:f294f3474d35b1a9 |
	// +------------+---------+---------------+----------------------------------+------------------------+

	// the expected results should mimic what is found as v5 namespace values:
	// +--------------------------------------+
	// | namespace                            |
	// +--------------------------------------+
	// | nvd:cpe                              |
	// | github:language:javascript           |
	// | ubuntu:distro:ubuntu:14.04           |
	// | ubuntu:distro:ubuntu:16.04           |
	// | ubuntu:distro:ubuntu:18.04           |
	// | ubuntu:distro:ubuntu:20.04           |
	// | ubuntu:distro:ubuntu:22.04           |
	// | ubuntu:distro:ubuntu:22.10           |
	// | ubuntu:distro:ubuntu:23.04           |
	// | ubuntu:distro:ubuntu:23.10           |
	// | ubuntu:distro:ubuntu:24.10           |
	// | debian:distro:debian:8               |
	// | debian:distro:debian:9               |
	// | ubuntu:distro:ubuntu:12.04           |
	// | ubuntu:distro:ubuntu:15.04           |
	// | sles:distro:sles:15                  |
	// | sles:distro:sles:15.1                |
	// | sles:distro:sles:15.2                |
	// | sles:distro:sles:15.3                |
	// | sles:distro:sles:15.4                |
	// | sles:distro:sles:15.5                |
	// | sles:distro:sles:15.6                |
	// | amazon:distro:amazonlinux:2          |
	// | debian:distro:debian:10              |
	// | debian:distro:debian:11              |
	// | debian:distro:debian:12              |
	// | debian:distro:debian:unstable        |
	// | oracle:distro:oraclelinux:6          |
	// | oracle:distro:oraclelinux:7          |
	// | oracle:distro:oraclelinux:8          |
	// | oracle:distro:oraclelinux:9          |
	// | redhat:distro:redhat:6               |
	// | redhat:distro:redhat:7               |
	// | redhat:distro:redhat:8               |
	// | redhat:distro:redhat:9               |
	// | ubuntu:distro:ubuntu:12.10           |
	// | ubuntu:distro:ubuntu:13.04           |
	// | ubuntu:distro:ubuntu:14.10           |
	// | ubuntu:distro:ubuntu:15.10           |
	// | ubuntu:distro:ubuntu:16.10           |
	// | ubuntu:distro:ubuntu:17.04           |
	// | ubuntu:distro:ubuntu:17.10           |
	// | ubuntu:distro:ubuntu:18.10           |
	// | ubuntu:distro:ubuntu:19.04           |
	// | ubuntu:distro:ubuntu:19.10           |
	// | ubuntu:distro:ubuntu:20.10           |
	// | ubuntu:distro:ubuntu:21.04           |
	// | ubuntu:distro:ubuntu:21.10           |
	// | ubuntu:distro:ubuntu:24.04           |
	// | github:language:php                  |
	// | debian:distro:debian:13              |
	// | debian:distro:debian:7               |
	// | redhat:distro:redhat:5               |
	// | sles:distro:sles:11.1                |
	// | sles:distro:sles:11.3                |
	// | sles:distro:sles:11.4                |
	// | sles:distro:sles:11.2                |
	// | sles:distro:sles:12                  |
	// | sles:distro:sles:12.1                |
	// | sles:distro:sles:12.2                |
	// | sles:distro:sles:12.3                |
	// | sles:distro:sles:12.4                |
	// | sles:distro:sles:12.5                |
	// | chainguard:distro:chainguard:rolling |
	// | wolfi:distro:wolfi:rolling           |
	// | minimos:distro:minimos:rolling       |
	// | github:language:go                   |
	// | alpine:distro:alpine:3.20            |
	// | alpine:distro:alpine:3.21            |
	// | alpine:distro:alpine:edge            |
	// | github:language:rust                 |
	// | github:language:python               |
	// | sles:distro:sles:11                  |
	// | oracle:distro:oraclelinux:5          |
	// | github:language:ruby                 |
	// | github:language:dotnet               |
	// | alpine:distro:alpine:3.12            |
	// | alpine:distro:alpine:3.13            |
	// | alpine:distro:alpine:3.14            |
	// | alpine:distro:alpine:3.15            |
	// | alpine:distro:alpine:3.16            |
	// | alpine:distro:alpine:3.17            |
	// | alpine:distro:alpine:3.18            |
	// | alpine:distro:alpine:3.19            |
	// | mariner:distro:mariner:2.0           |
	// | github:language:java                 |
	// | github:language:dart                 |
	// | amazon:distro:amazonlinux:2023       |
	// | alpine:distro:alpine:3.10            |
	// | alpine:distro:alpine:3.11            |
	// | alpine:distro:alpine:3.4             |
	// | alpine:distro:alpine:3.5             |
	// | alpine:distro:alpine:3.7             |
	// | alpine:distro:alpine:3.8             |
	// | alpine:distro:alpine:3.9             |
	// | mariner:distro:azurelinux:3.0        |
	// | mariner:distro:mariner:1.0           |
	// | alpine:distro:alpine:3.3             |
	// | alpine:distro:alpine:3.6             |
	// | amazon:distro:amazonlinux:2022       |
	// | alpine:distro:alpine:3.2             |
	// | github:language:swift                |
	// +--------------------------------------+

	type testCase struct {
		name        string
		provider    string // from Providers.id
		ecosystem   string // only used when provider non-os provider
		packageName string // only used for msrc
		osName      string // only used for OS-based providers
		osVersion   string // only used for OS-based providers
		expected    string //
	}

	tests := []testCase{
		// NVD
		{
			name:     "nvd provider",
			provider: "nvd",
			expected: "nvd:cpe",
		},

		// GitHub ecosystem tests
		{
			name:      "github golang direct",
			provider:  "github",
			ecosystem: "golang",
			expected:  "github:language:go",
		},
		{
			name:      "github go-module ecosystem",
			provider:  "github",
			ecosystem: "go-module",
			expected:  "github:language:go",
		},
		{
			name:      "github composer ecosystem",
			provider:  "github",
			ecosystem: "composer",
			expected:  "github:language:php",
		},
		{
			name:      "github php-composer ecosystem",
			provider:  "github",
			ecosystem: "php-composer",
			expected:  "github:language:php",
		},
		{
			name:      "github cargo ecosystem",
			provider:  "github",
			ecosystem: "cargo",
			expected:  "github:language:rust",
		},
		{
			name:      "github rust-crate ecosystem",
			provider:  "github",
			ecosystem: "rust-crate",
			expected:  "github:language:rust",
		},
		{
			name:      "github pub ecosystem",
			provider:  "github",
			ecosystem: "pub",
			expected:  "github:language:dart",
		},
		{
			name:      "github dart-pub ecosystem",
			provider:  "github",
			ecosystem: "dart-pub",
			expected:  "github:language:dart",
		},
		{
			name:      "github nuget ecosystem",
			provider:  "github",
			ecosystem: "nuget",
			expected:  "github:language:dotnet",
		},
		{
			name:      "github maven ecosystem",
			provider:  "github",
			ecosystem: "maven",
			expected:  "github:language:java",
		},
		{
			name:      "github java ecosystem",
			provider:  "github",
			ecosystem: "java",
			expected:  "github:language:java",
		},
		{
			name:      "syft pkg type java-archive",
			provider:  "github",
			ecosystem: "java-archive",
			expected:  "github:language:java",
		},
		{
			name:      "github swifturl ecosystem",
			provider:  "github",
			ecosystem: "swifturl",
			expected:  "github:language:swift",
		},
		{
			name:      "github npm ecosystem",
			provider:  "github",
			ecosystem: "npm",
			expected:  "github:language:javascript",
		},
		{
			name:      "github node ecosystem",
			provider:  "github",
			ecosystem: "node",
			expected:  "github:language:javascript",
		},
		{
			name:      "github pypi ecosystem",
			provider:  "github",
			ecosystem: "pypi",
			expected:  "github:language:python",
		},
		{
			name:      "github pip ecosystem",
			provider:  "github",
			ecosystem: "pip",
			expected:  "github:language:python",
		},
		{
			name:      "github rubygems ecosystem",
			provider:  "github",
			ecosystem: "rubygems",
			expected:  "github:language:ruby",
		},
		{
			name:      "github gem ecosystem",
			provider:  "github",
			ecosystem: "gem",
			expected:  "github:language:ruby",
		},

		// OS Distribution tests
		{
			name:      "ubuntu distribution",
			provider:  "ubuntu",
			osName:    "ubuntu",
			osVersion: "22.04",
			expected:  "ubuntu:distro:ubuntu:22.04",
		},
		{
			name:      "ubuntu distribution (trimmed 0s)",
			provider:  "ubuntu",
			osName:    "ubuntu",
			osVersion: "22.4",
			expected:  "ubuntu:distro:ubuntu:22.04",
		},
		{
			name:      "redhat distribution",
			provider:  "rhel",
			osName:    "redhat",
			osVersion: "8",
			expected:  "redhat:distro:redhat:8",
		},
		{
			name:      "debian distribution",
			provider:  "debian",
			osName:    "debian",
			osVersion: "11",
			expected:  "debian:distro:debian:11",
		},
		{
			name:      "sles distribution",
			provider:  "sles",
			osName:    "sles",
			osVersion: "15.5",
			expected:  "sles:distro:sles:15.5",
		},
		{
			name:      "alpine distribution",
			provider:  "alpine",
			osName:    "alpine",
			osVersion: "3.18",
			expected:  "alpine:distro:alpine:3.18",
		},
		{
			name:      "chainguard distribution",
			provider:  "chainguard",
			osName:    "chainguard",
			osVersion: "rolling",
			expected:  "chainguard:distro:chainguard:rolling",
		},
		{
			name:      "wolfi distribution",
			provider:  "wolfi",
			osName:    "wolfi",
			osVersion: "rolling",
			expected:  "wolfi:distro:wolfi:rolling",
		},
		{
			name:      "minimos distribution",
			provider:  "minimos",
			osName:    "minimos",
			osVersion: "rolling",
			expected:  "minimos:distro:minimos:rolling",
		},
		{
			name:      "amazon linux distribution",
			provider:  "amazon",
			osName:    "amazon",
			osVersion: "2023",
			expected:  "amazon:distro:amazonlinux:2023",
		},
		{
			name:      "mariner regular version",
			provider:  "mariner",
			osName:    "mariner",
			osVersion: "2.0",
			expected:  "mariner:distro:mariner:2.0",
		},
		{
			name:      "mariner regular version (not exact match)",
			provider:  "mariner",
			osName:    "mariner",
			osVersion: "2.1",
			expected:  "mariner:distro:mariner:2.1",
		},
		{
			name:      "mariner regular version (auto fill minor version)",
			provider:  "mariner",
			osName:    "mariner",
			osVersion: "1",
			expected:  "mariner:distro:mariner:1.0",
		},
		{
			name:      "mariner azure version",
			provider:  "mariner",
			osName:    "mariner",
			osVersion: "3.0",
			expected:  "mariner:distro:azurelinux:3.0",
		},
		{
			name:      "mariner azure version (missing version fields)",
			provider:  "mariner",
			osName:    "mariner",
			osVersion: "3",
			expected:  "mariner:distro:azurelinux:3.0",
		},
		{
			name:      "azurelinux version (extra version fields)",
			provider:  "mariner",
			osName:    "azurelinux",
			osVersion: "3.0.20240727",
			expected:  "mariner:distro:azurelinux:3.0",
		},
		{
			name:      "azurelinux version",
			provider:  "mariner",
			osName:    "azurelinux",
			osVersion: "3.0",
			expected:  "mariner:distro:azurelinux:3.0",
		},
		{
			name:      "azurelinux version (missing version fields)",
			provider:  "mariner",
			osName:    "azurelinux",
			osVersion: "3",
			expected:  "mariner:distro:azurelinux:3.0",
		},
		{
			name:      "mariner azure version (extra version fields)",
			provider:  "mariner",
			osName:    "mariner",
			osVersion: "3.0.20240727",
			expected:  "mariner:distro:azurelinux:3.0",
		},
		{
			name:      "oracle linux distribution",
			provider:  "oracle",
			osName:    "oracle",
			osVersion: "8",
			expected:  "oracle:distro:oraclelinux:8",
		},
		{
			name:      "echo distribution",
			provider:  "echo",
			osName:    "echo",
			osVersion: "rolling",
			expected:  "echo:distro:echo:rolling",
		},
		{
			name:      "minimos distribution",
			provider:  "minimos",
			osName:    "minimos",
			osVersion: "rolling",
			expected:  "minimos:distro:minimos:rolling",
		},

		// Version truncation tests
		{
			name:      "rhel with minor version",
			provider:  "rhel",
			osName:    "redhat",
			osVersion: "8.6",
			expected:  "redhat:distro:redhat:8",
		},
		{
			name:      "rhel with patch version",
			provider:  "rhel",
			osName:    "redhat",
			osVersion: "9.2.1",
			expected:  "redhat:distro:redhat:9",
		},
		{
			name:      "oracle with minor version",
			provider:  "oracle",
			osName:    "oracle",
			osVersion: "8.7",
			expected:  "oracle:distro:oraclelinux:8",
		},
		{
			name:      "oracle with patch version",
			provider:  "oracle",
			osName:    "oracle",
			osVersion: "9.3.1",
			expected:  "oracle:distro:oraclelinux:9",
		},
		// msrc is modeled as a distro for v5 but is just a package in v6
		{
			name:        "microsoft msrc-kb",
			provider:    "msrc",
			ecosystem:   "msrc-kb",
			packageName: "10012",
			expected:    "msrc:distro:windows:10012",
		},

		// new provider existing ecosystem
		{
			name:      "grizzly go-module",
			provider:  "grizzly",
			ecosystem: "go-module",
			expected:  "grizzly:language:go",
		},

		// new provider new ecosystem
		{
			name:      "armadillo pizza",
			provider:  "armadillo",
			ecosystem: "pizza",
			expected:  "armadillo:language:pizza",
		},

		// new OS
		{
			name:      "gothmog",
			provider:  "gothmog",
			osName:    "gothmoglinux",
			osVersion: "zzzzzz11123",
			expected:  "gothmog:distro:gothmoglinux:zzzzzz11123",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			vuln := &VulnerabilityHandle{
				Provider: &Provider{
					ID: tt.provider,
				},
			}

			pkg := &AffectedPackageHandle{}

			if tt.osName != "" {
				major, minor, _ := majorMinorPatch(tt.osVersion)
				var label string
				if major == "" {
					label = tt.osVersion
				}
				pkg.OperatingSystem = &OperatingSystem{
					Name:         tt.osName,
					MajorVersion: major,
					MinorVersion: minor,
					LabelVersion: label,
				}
				pkg.Package = &Package{
					Name:      "os-package",
					Ecosystem: "os-ecosystem",
				}
			} else if tt.ecosystem != "" {
				pkg.Package = &Package{
					Ecosystem: tt.ecosystem,
					Name:      tt.packageName,
				}
			}

			result := MimicV5Namespace(vuln, pkg)
			assert.Equal(t, tt.expected, result)
		})
	}
}

func Test_getVersionConstraint(t *testing.T) {
	tests := []struct {
		name        string
		ranges      []Range
		version     string
		expectMatch bool
	}{
		{
			name: "single constraint matches version below range",
			ranges: []Range{
				{Version: Version{Type: "rpm", Constraint: "< 1.0.0"}},
			},
			version:     "0.9.0",
			expectMatch: true,
		},
		{
			name: "single constraint does not match version at boundary",
			ranges: []Range{
				{Version: Version{Type: "rpm", Constraint: "< 1.0.0"}},
			},
			version:     "1.0.0",
			expectMatch: false,
		},
		{
			name: "single constraint does not match version above range",
			ranges: []Range{
				{Version: Version{Type: "rpm", Constraint: "< 1.0.0"}},
			},
			version:     "2.0.0",
			expectMatch: false,
		},
		{
			name: "multiple constraint ranges use OR operator",
			ranges: []Range{
				{Version: Version{Type: "rpm", Constraint: "< 1.0.0"}},
				{Version: Version{Type: "rpm", Constraint: ">= 1.4.0, < 2.0.0"}},
			},
			version:     "1.5.0",
			expectMatch: true,
		},
		{
			name: "multiple constraint ranges outside the ranges",
			ranges: []Range{
				{Version: Version{Type: "rpm", Constraint: "< 1.0.0"}},
				{Version: Version{Type: "rpm", Constraint: ">= 1.4.0, < 2.0.0"}},
			},
			version:     "1.1.0",
			expectMatch: false,
		},
		{
			name: "many constraint ranges within range 1",
			ranges: []Range{
				{Version: Version{Type: "rpm", Constraint: "< 1.0.0"}},
				{Version: Version{Type: "rpm", Constraint: ">= 1.4.0, < 2.0.0"}},
				{Version: Version{Type: "rpm", Constraint: ">= 3.0.0"}},
			},
			version:     "0.9.0",
			expectMatch: true,
		},
		{
			name: "many constraint ranges within range 2",
			ranges: []Range{
				{Version: Version{Type: "rpm", Constraint: "< 1.0.0"}},
				{Version: Version{Type: "rpm", Constraint: ">= 1.4.0, < 2.0.0"}},
				{Version: Version{Type: "rpm", Constraint: ">= 3.0.0"}},
			},
			version:     "1.9.0",
			expectMatch: true,
		},
		{
			name: "many constraint ranges within range 3",
			ranges: []Range{
				{Version: Version{Type: "rpm", Constraint: "< 1.0.0"}},
				{Version: Version{Type: "rpm", Constraint: ">= 1.4.0, < 2.0.0"}},
				{Version: Version{Type: "rpm", Constraint: ">= 3.0.0"}},
			},
			version:     "3.1.0",
			expectMatch: true,
		},
		{
			name: "many constraint ranges outside the ranges",
			ranges: []Range{
				{Version: Version{Type: "rpm", Constraint: "< 1.0.0"}},
				{Version: Version{Type: "rpm", Constraint: ">= 1.4.0, < 2.0.0"}},
				{Version: Version{Type: "rpm", Constraint: ">= 3.0.0"}},
			},
			version:     "2.1.0",
			expectMatch: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			constraint, err := getVersionConstraint(tt.ranges)
			require.NoError(t, err)
			require.NotNil(t, constraint)

			ver := version.New(tt.version, version.ParseFormat(tt.ranges[0].Version.Type))
			satisfied, err := constraint.Satisfied(ver)
			require.NoError(t, err)
			assert.Equal(t, tt.expectMatch, satisfied, "version %s against constraint %s", tt.version, constraint.String())
		})
	}
}

func Test_getRelatedVulnerabilities(t *testing.T) {
	tests := []struct {
		name     string
		vuln     VulnerabilityHandle
		affected PackageBlob
		language string
		expected []vulnerability.Reference
	}{
		{
			name: "GHSA with related CVEs",
			vuln: VulnerabilityHandle{
				Name:       "GHSA-1234",
				ProviderID: githubProvider,
				BlobValue: &VulnerabilityBlob{
					Aliases: []string{"CVE-2024-1"},
				},
			},
			affected: PackageBlob{
				CVEs: []string{"CVE-2024-2", "CVE-2024-3"},
			},
			language: "python",
			expected: []vulnerability.Reference{
				{ID: "CVE-2024-1", Namespace: v5NvdNamespace},
				{ID: "CVE-2024-2", Namespace: v5NvdNamespace},
				{ID: "CVE-2024-3", Namespace: v5NvdNamespace},
			},
		},
		{
			name: "CGA with related GHSA",
			vuln: VulnerabilityHandle{
				Name:       "CGA-1234",
				ProviderID: githubProvider,
				BlobValue: &VulnerabilityBlob{
					Aliases: []string{"GHSA-1234"},
				},
			},
			affected: PackageBlob{
				CVEs: []string{"CVE-2024-2", "CVE-2024-3"},
			},
			language: "python",
			expected: []vulnerability.Reference{
				{ID: "GHSA-1234", Namespace: "github:language:python"},
				{ID: "CVE-2024-2", Namespace: v5NvdNamespace},
				{ID: "CVE-2024-3", Namespace: v5NvdNamespace},
			},
		},
		{
			name: "CVE with related CVEs",
			vuln: VulnerabilityHandle{
				Name:       "CVE-2024-1234",
				ProviderID: "rhel",
				BlobValue: &VulnerabilityBlob{
					Aliases: []string{"CVE-2024-1"},
				},
			},
			affected: PackageBlob{
				CVEs: []string{"CVE-2024-2", "CVE-2024-3"},
			},
			expected: []vulnerability.Reference{
				{ID: "CVE-2024-1234", Namespace: v5NvdNamespace},
				{ID: "CVE-2024-1", Namespace: v5NvdNamespace},
				{ID: "CVE-2024-2", Namespace: v5NvdNamespace},
				{ID: "CVE-2024-3", Namespace: v5NvdNamespace},
			},
		},
		{
			name: "nvd CVE skips related CVEs to self",
			vuln: VulnerabilityHandle{
				Name:       "CVE-2024-1234",
				ProviderID: nvdProvider,
				BlobValue: &VulnerabilityBlob{
					Aliases: []string{"CVE-2024-1", "CVE-2024-1234"},
				},
			},
			affected: PackageBlob{
				CVEs: []string{"CVE-2024-2", "CVE-2024-1234"},
			},
			expected: []vulnerability.Reference{
				{ID: "CVE-2024-1", Namespace: v5NvdNamespace},
				{ID: "CVE-2024-2", Namespace: v5NvdNamespace},
			}, // does not include "CVE-2024-1234"
		},
		{
			name: "non-nvd CVE with related nvd CVEs to self",
			vuln: VulnerabilityHandle{
				Name:       "CVE-2024-1234",
				ProviderID: "rhel",
				BlobValue: &VulnerabilityBlob{
					Aliases: []string{"CVE-2024-1", "CVE-2024-1234"},
				},
			},
			affected: PackageBlob{
				CVEs: []string{"CVE-2024-2", "CVE-2024-1234"},
			},
			expected: []vulnerability.Reference{
				{ID: "CVE-2024-1", Namespace: v5NvdNamespace},
				{ID: "CVE-2024-2", Namespace: v5NvdNamespace},
				{ID: "CVE-2024-1234", Namespace: v5NvdNamespace},
			}, // does include "CVE-2024-1234"
		},
		{
			name: "non-nvd CVE always relates back to NVD",
			vuln: VulnerabilityHandle{
				Name:       "CVE-2024-1234",
				ProviderID: "rhel",
				BlobValue: &VulnerabilityBlob{
					Aliases: []string{},
				},
			},
			affected: PackageBlob{
				CVEs: []string{},
			},
			expected: []vulnerability.Reference{
				{ID: "CVE-2024-1234", Namespace: v5NvdNamespace},
			}, // does include "CVE-2024-1234"
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := getRelatedVulnerabilities(&tt.vuln, &tt.affected, tt.language)
			require.ElementsMatch(t, tt.expected, got)
		})
	}
}

func TestToFix(t *testing.T) {
	fixedDate := time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC)
	anotherDate := time.Date(2024, 2, 1, 12, 0, 0, 0, time.UTC)

	tests := []struct {
		name           string
		affectedRanges []Range
		expectedFix    vulnerability.Fix
	}{
		{
			name:           "empty affected ranges",
			affectedRanges: []Range{},
			expectedFix:    vulnerability.Fix{},
		},
		{
			name: "all ranges have nil fix",
			affectedRanges: []Range{
				{Fix: nil},
				{Fix: nil},
			},
			expectedFix: vulnerability.Fix{},
		},
		{
			name: "single fixed version without available info",
			affectedRanges: []Range{
				{
					Fix: &Fix{
						Version: "1.2.3",
						State:   FixedStatus,
					},
				},
			},
			expectedFix: vulnerability.Fix{
				Versions:  []string{"1.2.3"},
				State:     vulnerability.FixStateFixed,
				Available: nil,
			},
		},
		{
			name: "single fixed version with complete available info",
			affectedRanges: []Range{
				{
					Fix: &Fix{
						Version: "1.2.3",
						State:   FixedStatus,
						Detail: &FixDetail{
							Available: &FixAvailability{
								Date: &fixedDate,
								Kind: "first-observed",
							},
						},
					},
				},
			},
			expectedFix: vulnerability.Fix{
				Versions: []string{"1.2.3"},
				State:    vulnerability.FixStateFixed,
				Available: []vulnerability.FixAvailable{
					{
						Version: "1.2.3",
						Date:    fixedDate,
						Kind:    "first-observed",
					},
				},
			},
		},
		{
			name: "fix detail is nil - no available info",
			affectedRanges: []Range{
				{
					Fix: &Fix{
						Version: "1.2.3",
						State:   FixedStatus,
						Detail:  nil,
					},
				},
			},
			expectedFix: vulnerability.Fix{
				Versions:  []string{"1.2.3"},
				State:     vulnerability.FixStateFixed,
				Available: nil,
			},
		},
		{
			name: "fix detail available is nil - no available info",
			affectedRanges: []Range{
				{
					Fix: &Fix{
						Version: "1.2.3",
						State:   FixedStatus,
						Detail: &FixDetail{
							Available: nil,
						},
					},
				},
			},
			expectedFix: vulnerability.Fix{
				Versions:  []string{"1.2.3"},
				State:     vulnerability.FixStateFixed,
				Available: nil,
			},
		},
		{
			name: "fix detail available date is nil - no available info",
			affectedRanges: []Range{
				{
					Fix: &Fix{
						Version: "1.2.3",
						State:   FixedStatus,
						Detail: &FixDetail{
							Available: &FixAvailability{
								Date: nil,
								Kind: "first-observed",
							},
						},
					},
				},
			},
			expectedFix: vulnerability.Fix{
				Versions:  []string{"1.2.3"},
				State:     vulnerability.FixStateFixed,
				Available: nil,
			},
		},
		{
			name: "multiple fixed versions with mixed available info",
			affectedRanges: []Range{
				{
					Fix: &Fix{
						Version: "1.2.3",
						State:   FixedStatus,
						Detail: &FixDetail{
							Available: &FixAvailability{
								Date: &fixedDate,
								Kind: "first-observed",
							},
						},
					},
				},
				{
					Fix: &Fix{
						Version: "1.3.0",
						State:   FixedStatus,
						Detail: &FixDetail{
							Available: &FixAvailability{
								Date: nil, // this should not create an available entry
								Kind: "first-observed",
							},
						},
					},
				},
				{
					Fix: &Fix{
						Version: "1.4.0",
						State:   FixedStatus,
						Detail: &FixDetail{
							Available: &FixAvailability{
								Date: &anotherDate,
								Kind: "first-observed",
							},
						},
					},
				},
			},
			expectedFix: vulnerability.Fix{
				Versions: []string{"1.2.3", "1.3.0", "1.4.0"},
				State:    vulnerability.FixStateFixed,
				Available: []vulnerability.FixAvailable{
					{
						Version: "1.2.3",
						Date:    fixedDate,
						Kind:    "first-observed",
					},
					{
						Version: "1.4.0",
						Date:    anotherDate,
						Kind:    "first-observed",
					},
				},
			},
		},
		{
			name: "wont fix status only",
			affectedRanges: []Range{
				{
					Fix: &Fix{
						Version: "1.2.3",
						State:   WontFixStatus,
					},
				},
			},
			expectedFix: vulnerability.Fix{
				Versions:  nil,
				State:     vulnerability.FixStateWontFix,
				Available: nil,
			},
		},
		{
			name: "not fixed status only",
			affectedRanges: []Range{
				{
					Fix: &Fix{
						Version: "1.2.3",
						State:   NotFixedStatus,
					},
				},
			},
			expectedFix: vulnerability.Fix{
				Versions:  nil,
				State:     vulnerability.FixStateNotFixed,
				Available: nil,
			},
		},
		{
			name: "not affected status - not handled yet",
			affectedRanges: []Range{
				{
					Fix: &Fix{
						Version: "1.2.3",
						State:   NotAffectedFixStatus,
					},
				},
			},
			expectedFix: vulnerability.Fix{},
		},
		{
			name: "fixed status overrides wont fix",
			affectedRanges: []Range{
				{
					Fix: &Fix{
						Version: "1.2.3",
						State:   WontFixStatus,
					},
				},
				{
					Fix: &Fix{
						Version: "1.3.0",
						State:   FixedStatus,
					},
				},
			},
			expectedFix: vulnerability.Fix{
				Versions:  []string{"1.3.0"},
				State:     vulnerability.FixStateFixed,
				Available: nil,
			},
		},
		{
			name: "fixed status overrides not fixed",
			affectedRanges: []Range{
				{
					Fix: &Fix{
						Version: "1.2.3",
						State:   NotFixedStatus,
					},
				},
				{
					Fix: &Fix{
						Version: "1.3.0",
						State:   FixedStatus,
					},
				},
			},
			expectedFix: vulnerability.Fix{
				Versions:  []string{"1.3.0"},
				State:     vulnerability.FixStateFixed,
				Available: nil,
			},
		},
		{
			name: "wont fix overrides not fixed",
			affectedRanges: []Range{
				{
					Fix: &Fix{
						Version: "1.2.3",
						State:   NotFixedStatus,
					},
				},
				{
					Fix: &Fix{
						Version: "1.3.0",
						State:   WontFixStatus,
					},
				},
			},
			expectedFix: vulnerability.Fix{
				Versions:  nil,
				State:     vulnerability.FixStateWontFix,
				Available: nil,
			},
		},
		{
			name: "mix of nil fixes and various states",
			affectedRanges: []Range{
				{Fix: nil},
				{
					Fix: &Fix{
						Version: "1.2.3",
						State:   WontFixStatus,
					},
				},
				{Fix: nil},
				{
					Fix: &Fix{
						Version: "1.3.0",
						State:   FixedStatus,
						Detail: &FixDetail{
							Available: &FixAvailability{
								Date: &fixedDate,
								Kind: "first-observed",
							},
						},
					},
				},
			},
			expectedFix: vulnerability.Fix{
				Versions: []string{"1.3.0"},
				State:    vulnerability.FixStateFixed,
				Available: []vulnerability.FixAvailable{
					{
						Version: "1.3.0",
						Date:    fixedDate,
						Kind:    "first-observed",
					},
				},
			},
		},
		{
			name: "available with empty kind field",
			affectedRanges: []Range{
				{
					Fix: &Fix{
						Version: "1.2.3",
						State:   FixedStatus,
						Detail: &FixDetail{
							Available: &FixAvailability{
								Date: &fixedDate,
								Kind: "", // empty kind should still work
							},
						},
					},
				},
			},
			expectedFix: vulnerability.Fix{
				Versions: []string{"1.2.3"},
				State:    vulnerability.FixStateFixed,
				Available: []vulnerability.FixAvailable{
					{
						Version: "1.2.3",
						Date:    fixedDate,
						Kind:    "",
					},
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := toFix(tt.affectedRanges)
			require.Equal(t, tt.expectedFix, got)
		})
	}
}

func majorMinorPatch(ver string) (string, string, string) {
	if !unicode.IsDigit(rune(ver[0])) {
		return "", "", ""
	}
	parts := strings.Split(ver, ".")
	if len(parts) == 0 {
		return "", "", ""
	}
	if len(parts) == 1 {
		return parts[0], "", ""
	}
	if len(parts) == 2 {
		return parts[0], parts[1], ""
	}
	return parts[0], parts[1], parts[2]
}
