package v6

import (
	"reflect"
	"slices"
	"testing"
	"time"

	"github.com/google/go-cmp/cmp"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/anchore/grype/internal/schemaver"
)

func TestVulnerabilityDecoratorStore(t *testing.T) {
	tests := []struct {
		name        string
		kevEnabled  bool
		setupStore  func(*vulnerabilityDecoratorStore) error
		input       []*KnownExploitedVulnerabilityHandle
		expectError require.ErrorAssertionFunc
	}{
		{
			name:       "happy path - single KEV",
			kevEnabled: true,
			input: []*KnownExploitedVulnerabilityHandle{
				{
					Cve: "CVE-2023-1234",
					BlobValue: &KnownExploitedVulnerabilityBlob{
						Cve:           "CVE-2023-1234",
						VendorProject: "Test Vendor",
						Product:       "Test Product",
						DateAdded:     timeRef(time.Now()),
					},
				},
			},
		},
		{
			name:       "happy path - multiple KEVs",
			kevEnabled: true,
			input: []*KnownExploitedVulnerabilityHandle{
				{
					Cve: "CVE-2023-1234",
					BlobValue: &KnownExploitedVulnerabilityBlob{
						Cve:           "CVE-2023-1234",
						VendorProject: "Vendor 1",
					},
				},
				{
					Cve: "CVE-2023-5678",
					BlobValue: &KnownExploitedVulnerabilityBlob{
						Cve:           "CVE-2023-5678",
						VendorProject: "Vendor 2",
					},
				},
			},
		},
		{
			name:        "error - KEV disabled",
			kevEnabled:  false,
			input:       []*KnownExploitedVulnerabilityHandle{{Cve: "CVE-2023-1234"}},
			expectError: require.Error,
		},
		{
			name:       "duplicate CVEs (unexpected but allowed)",
			kevEnabled: true,
			input: []*KnownExploitedVulnerabilityHandle{
				{
					Cve: "CVE-2023-1234",
					BlobValue: &KnownExploitedVulnerabilityBlob{
						Cve:            "CVE-2023-1234",
						RequiredAction: "1",
					},
				},
				{
					Cve: "CVE-2023-1234",
					BlobValue: &KnownExploitedVulnerabilityBlob{
						Cve:            "CVE-2023-1234",
						RequiredAction: "2",
					},
				},
			},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if tt.expectError == nil {
				tt.expectError = require.NoError
			}

			db := setupTestStore(t).db
			bs := newBlobStore(db)

			s := &vulnerabilityDecoratorStore{
				db:        db,
				blobStore: bs,
				vulnerabilityDecoratorCapabilities: vulnerabilityDecoratorCapabilities{
					kevEnabled: tt.kevEnabled,
				},
			}

			if tt.setupStore != nil {
				require.NoError(t, tt.setupStore(s))
			}

			err := s.AddKnownExploitedVulnerabilities(tt.input...)
			tt.expectError(t, err)
			if err != nil {
				return
			}

			var cves []string
			for _, kev := range tt.input {
				if !slices.Contains(cves, kev.Cve) {
					cves = append(cves, kev.Cve)
				}
			}

			var actual []*KnownExploitedVulnerabilityHandle
			for _, cve := range cves {
				intermediate, err := s.GetKnownExploitedVulnerabilities(cve)
				require.NoError(t, err)
				for i := range intermediate {
					actual = append(actual, &intermediate[i])
				}
			}

			for _, a := range actual {
				assert.NotZero(t, a.ID)
				assert.NotZero(t, a.BlobID)
			}

			if d := cmp.Diff(tt.input, actual); d != "" {
				t.Errorf("unexpected known exploited vulnerabilities (-expected, +actual): %s", d)
			}
		})
	}
}

func TestVulnerabilityDecoratorStore_AddKnownExploitedVulnerabilities_VersionCompatibility(t *testing.T) {
	tests := []struct {
		name          string
		dbVersion     schemaver.SchemaVer
		input         []*KnownExploitedVulnerabilityHandle
		expectEnabled bool
		expectError   require.ErrorAssertionFunc
		expectedCount int
	}{
		{
			name:      "supported db version",
			dbVersion: schemaver.New(6, 0, 1),
			input: []*KnownExploitedVulnerabilityHandle{
				{
					Cve: "CVE-2023-1234",
					BlobValue: &KnownExploitedVulnerabilityBlob{
						Cve:           "CVE-2023-1234",
						VendorProject: "Test Vendor",
						DateAdded:     timeRef(time.Now()),
					},
				},
			},
			expectEnabled: true,
			expectError:   require.NoError,
			expectedCount: 1,
		},
		{
			name:      "unsupported db version",
			dbVersion: schemaver.New(6, 0, 0),
			input: []*KnownExploitedVulnerabilityHandle{
				{
					Cve: "CVE-2023-1234",
					BlobValue: &KnownExploitedVulnerabilityBlob{
						Cve: "CVE-2023-1234",
					},
				},
			},
			expectEnabled: false,
			expectError:   require.Error,
			expectedCount: 0,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if tt.expectError == nil {
				tt.expectError = require.NoError
			}

			db := setupTestStore(t).db
			bs := newBlobStore(db)

			s := newVulnerabilityDecoratorStore(db, bs, tt.dbVersion)
			assert.Equal(t, tt.expectEnabled, s.kevEnabled)

			err := s.AddKnownExploitedVulnerabilities(tt.input...)
			tt.expectError(t, err)
			if err != nil {
				return
			}

			results, err := s.GetKnownExploitedVulnerabilities(tt.input[0].Cve)
			require.NoError(t, err)
			assert.Len(t, results, tt.expectedCount)
		})
	}
}

func TestVulnerabilityDecoratorStore_GetKnownExploitedVulnerabilities_VersionCompatibility(t *testing.T) {
	tests := []struct {
		name          string
		dbVersion     schemaver.SchemaVer
		input         []*KnownExploitedVulnerabilityHandle
		expectEnabled bool
		expectError   require.ErrorAssertionFunc
		expectedCount int
	}{
		{
			name:      "supported db version",
			dbVersion: schemaver.New(6, 0, 1),
			input: []*KnownExploitedVulnerabilityHandle{
				{
					Cve: "CVE-2023-1234",
					BlobValue: &KnownExploitedVulnerabilityBlob{
						Cve: "CVE-2023-1234",
					},
				},
			},
			expectEnabled: true,
			expectError:   require.NoError,
			expectedCount: 1,
		},
		{
			name:      "unsupported db version",
			dbVersion: schemaver.New(6, 0, 0),
			input: []*KnownExploitedVulnerabilityHandle{
				{
					Cve: "CVE-2023-1234",
					BlobValue: &KnownExploitedVulnerabilityBlob{
						Cve: "CVE-2023-1234",
					},
				},
			},
			expectEnabled: false,
			expectError:   require.NoError,
			expectedCount: 0,
		},
		{
			name:      "future db version",
			dbVersion: schemaver.New(6, 1, 0),
			input: []*KnownExploitedVulnerabilityHandle{
				{
					Cve: "CVE-2023-1234",
					BlobValue: &KnownExploitedVulnerabilityBlob{
						Cve: "CVE-2023-1234",
					},
				},
			},
			expectEnabled: true,
			expectError:   require.NoError,
			expectedCount: 1,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if tt.expectError == nil {
				tt.expectError = require.NoError
			}

			db := setupTestStore(t).db
			bs := newBlobStore(db)

			s := newVulnerabilityDecoratorStore(db, bs, tt.dbVersion)
			assert.Equal(t, tt.expectEnabled, s.kevEnabled)

			// this is just to get around not being able to write entries...
			supportedStore := newVulnerabilityDecoratorStore(db, bs, schemaver.New(6, 0, 1))
			err := supportedStore.AddKnownExploitedVulnerabilities(tt.input...)
			require.NoError(t, err)

			results, err := s.GetKnownExploitedVulnerabilities(tt.input[0].Cve)
			tt.expectError(t, err)
			assert.Len(t, results, tt.expectedCount)

			if tt.expectedCount > 0 {
				for _, result := range results {
					assert.NotNil(t, result.BlobValue)
					assert.Equal(t, tt.input[0].Cve, result.BlobValue.Cve)
				}
			}
		})
	}
}

func TestVulnerabilityDecoratorCapabilities_AllCapabilitiesCovered(t *testing.T) {
	tests := []struct {
		capability          string
		minSupportedVersion schemaver.SchemaVer
		fieldName           string
	}{
		{
			capability:          "KEV",
			minSupportedVersion: schemaver.New(6, 0, 1),
			fieldName:           "kevEnabled",
		},
		{
			capability:          "EPSS",
			minSupportedVersion: schemaver.New(6, 0, 2),
			fieldName:           "epssEnabled",
		},
		{
			capability:          "CWE",
			minSupportedVersion: schemaver.New(6, 1, 2),
			fieldName:           "cweEnabled",
		},
	}

	// Verify all fields in vulnerabilityDecoratorCapabilities are covered
	capabilitiesType := reflect.TypeFor[vulnerabilityDecoratorCapabilities]()
	fieldCount := capabilitiesType.NumField()

	if fieldCount != len(tests) {
		t.Errorf("vulnerabilityDecoratorCapabilities has %d fields but only %d test cases. All fields must be covered.", fieldCount, len(tests))
	}

	coveredFields := make(map[string]bool)
	for _, tt := range tests {
		coveredFields[tt.fieldName] = true
	}

	for i := range fieldCount {
		field := capabilitiesType.Field(i)
		if !coveredFields[field.Name] {
			t.Errorf("Field %q in vulnerabilityDecoratorCapabilities is not covered by any test case", field.Name)
		}
	}

	// Test each capability at lower, equal, and higher versions
	for _, tt := range tests {
		t.Run(tt.capability+"_version_checks", func(t *testing.T) {
			db := setupTestStore(t).db
			bs := newBlobStore(db)

			// Test 1: Lower version - capability should be OFF
			lowerVersion := schemaver.New(
				tt.minSupportedVersion.Model,
				tt.minSupportedVersion.Revision,
				tt.minSupportedVersion.Addition-1,
			)
			t.Run("lower_version", func(t *testing.T) {
				store := newVulnerabilityDecoratorStore(db, bs, lowerVersion)
				capValue := reflect.ValueOf(store.vulnerabilityDecoratorCapabilities).FieldByName(tt.fieldName)
				assert.False(t, capValue.Bool(), "capability %s should be disabled for version %s (below %s)", tt.capability, lowerVersion, tt.minSupportedVersion)
			})

			// Test 2: Equal version - capability should be ON
			t.Run("equal_version", func(t *testing.T) {
				store := newVulnerabilityDecoratorStore(db, bs, tt.minSupportedVersion)
				capValue := reflect.ValueOf(store.vulnerabilityDecoratorCapabilities).FieldByName(tt.fieldName)
				assert.True(t, capValue.Bool(), "capability %s should be enabled for version %s", tt.capability, tt.minSupportedVersion)
			})

			// Test 3: Higher version - capability should be ON
			higherVersion := schemaver.New(
				tt.minSupportedVersion.Model,
				tt.minSupportedVersion.Revision+1,
				0,
			)
			t.Run("higher_version", func(t *testing.T) {
				store := newVulnerabilityDecoratorStore(db, bs, higherVersion)
				capValue := reflect.ValueOf(store.vulnerabilityDecoratorCapabilities).FieldByName(tt.fieldName)
				assert.True(t, capValue.Bool(), "capability %s should be enabled for version %s (above %s)", tt.capability, higherVersion, tt.minSupportedVersion)
			})
		})
	}
}

func timeRef(t time.Time) *time.Time {
	return &t
}
