package cyclonedx

import (
	"testing"

	"github.com/CycloneDX/cyclonedx-go"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/anchore/grype/grype/presenter/models"
	"github.com/anchore/grype/grype/vulnerability"
)

func TestCvssVersionToMethod(t *testing.T) {
	testCases := []struct {
		desc     string
		input    string
		expected cyclonedx.ScoringMethod
		errors   bool
	}{
		{
			desc:     "invalid (not float)",
			input:    "",
			expected: "",
			errors:   true,
		},
		{
			desc:     "CVSS v2",
			input:    "2.0",
			expected: cyclonedx.ScoringMethodCVSSv2,
			errors:   false,
		},
		{
			desc:     "CVSS v31",
			input:    "3.1",
			expected: cyclonedx.ScoringMethodCVSSv31,
			errors:   false,
		},
		{
			desc:     "CVSS v3",
			input:    "3",
			expected: cyclonedx.ScoringMethodCVSSv3,
			errors:   false,
		},
		{
			desc:     "invalid (no match)",
			input:    "15.4",
			expected: cyclonedx.ScoringMethodOther,
			errors:   false,
		},
	}
	for _, tc := range testCases {
		t.Run(tc.desc, func(t *testing.T) {
			actual, err := cvssVersionToMethod(tc.input)
			if !tc.errors {
				assert.NoError(t, err)
			} else {
				assert.Error(t, err)
			}

			assert.Equal(t, tc.expected, actual)
		})
	}
}

type metadataProvider struct {
	severity string
	cvss     []vulnerability.Cvss
}

func (m metadataProvider) VulnerabilityMetadata(ref vulnerability.Reference) (*vulnerability.Metadata, error) {
	return &vulnerability.Metadata{
		ID:          ref.ID,
		DataSource:  "",
		Namespace:   ref.Namespace,
		Severity:    m.severity,
		URLs:        nil,
		Description: "",
		Cvss:        m.cvss,
	}, nil
}

func TestNewVulnerability_AlwaysIncludesSeverity(t *testing.T) {
	tests := []struct {
		name  string
		match models.Match
	}{
		{
			name: "populates severity with missing CVSS records",
			match: models.Match{
				Vulnerability: models.Vulnerability{
					VulnerabilityMetadata: models.VulnerabilityMetadata{
						Severity: "High",
					},
				},
				Artifact:     models.Package{},
				MatchDetails: nil,
			},
		},
		{
			name: "populates severity with all CVSS records",
			match: models.Match{
				Vulnerability: models.Vulnerability{
					VulnerabilityMetadata: models.VulnerabilityMetadata{
						Severity: "High",
						Cvss: []models.Cvss{
							{
								Version: "2.0",
								Metrics: models.CvssMetrics{
									BaseScore: 1.1,
								},
							},
							{
								Version: "3.0",
								Metrics: models.CvssMetrics{
									BaseScore: 2.1,
								},
							},
							{
								Version: "3.1",
								Metrics: models.CvssMetrics{
									BaseScore: 3.1,
								},
							},
						},
					},
				},
				Artifact:     models.Package{},
				MatchDetails: nil,
			},
		},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			actual, err := NewVulnerability(test.match)
			require.NoError(t, err)
			require.NotNil(t, actual.Ratings, "cyclonedx document ratings should not be nil")
			require.NotEmpty(t, actual.Ratings)
			require.Equal(t, cdxSeverityFromGrypeSeverity(test.match.Vulnerability.Severity), (*actual.Ratings)[0].Severity)
			if len(test.match.Vulnerability.Cvss) > 0 {
				for i, rating := range *actual.Ratings {
					require.Equal(t, test.match.Vulnerability.Cvss[i].Metrics.BaseScore, *rating.Score)
				}
			}
		})
	}
}

func TestNewVulnerability_IncludesEPSSAndKEV(t *testing.T) {
	match := models.Match{
		Vulnerability: models.Vulnerability{
			VulnerabilityMetadata: models.VulnerabilityMetadata{
				ID:       "CVE-2025-0001",
				Severity: "High",
				EPSS: []models.EPSS{
					{
						EPSS: 0.87,
					},
				},
				KnownExploited: []models.KnownExploited{
					{
						KnownRansomwareCampaignUse: "known",
					},
				},
			},
		},
		Artifact:     models.Package{},
		MatchDetails: nil,
	}

	vuln, err := NewVulnerability(match)
	require.NoError(t, err)

	ratings := *vuln.Ratings
	require.Len(t, ratings, 3, "should include 1 CVSS + 1 EPSS + 1 KEV rating")

	var foundEPSS, foundKEV bool
	for _, r := range ratings {
		if r.Method == "EPSS" {
			foundEPSS = true
			assert.NotNil(t, r.Score)
			assert.InDelta(t, 0.87, *r.Score, 0.001)
			assert.Equal(t, "FIRST", r.Source.Name)
		}
		if r.Method == "other" && r.Source != nil && r.Source.Name == "CISA KEV Catalog" {
			foundKEV = true
			assert.NotNil(t, r.Score)
			assert.Equal(t, 1.0, *r.Score)
		}
	}
	assert.True(t, foundEPSS, "should include EPSS rating")
	assert.True(t, foundKEV, "should include KEV rating")
}
