package v6

import (
	"testing"
	"time"

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

func TestVulnerabilityStore_AddVulnerabilities(t *testing.T) {
	db := setupTestStore(t).db
	bw := newBlobStore(db)
	s := newVulnerabilityStore(db, bw)

	vuln1 := VulnerabilityHandle{
		Name: "CVE-1234-5678",
		BlobValue: &VulnerabilityBlob{
			ID: "CVE-1234-5678",
		},
		Provider: &Provider{
			ID: "provider!",
		},
	}

	vuln2 := testVulnerabilityHandle()

	err := s.AddVulnerabilities(&vuln1, &vuln2)
	require.NoError(t, err)

	var result1 VulnerabilityHandle
	err = db.Where("name = ?", "CVE-1234-5678").First(&result1).Error
	require.NoError(t, err)
	assert.Equal(t, vuln1.Name, result1.Name)
	assert.Equal(t, vuln1.ID, result1.ID)
	assert.Equal(t, vuln1.BlobID, result1.BlobID)
	assert.Nil(t, result1.BlobValue) // since we're not preloading any fields on the fetch
	assert.Nil(t, result1.Provider)  // since we're not preloading any fields on the fetch

	var result2 VulnerabilityHandle
	err = db.Where("name = ?", "CVE-8765-4321").First(&result2).Error
	require.NoError(t, err)
	assert.Equal(t, vuln2.Name, result2.Name)
	assert.Equal(t, vuln2.ID, result2.ID)
	assert.Equal(t, vuln2.BlobID, result2.BlobID)
	assert.Nil(t, result2.BlobValue) // since we're not preloading any fields on the fetch
	assert.Nil(t, result1.Provider)  // since we're not preloading any fields on the fetch
}

func TestVulnerabilityStore_NoDuplicateVulnerabilities(t *testing.T) {
	db := setupTestStore(t).db
	bw := newBlobStore(db)
	s := newVulnerabilityStore(db, bw)

	vuln := VulnerabilityHandle{
		Name: "CVE-1234-5678",
		BlobValue: &VulnerabilityBlob{
			ID: "CVE-1234-5678",
		},
		Provider: &Provider{
			ID: "provider!",
		},
	}

	err := s.AddVulnerabilities(&vuln)
	require.NoError(t, err)

	err = s.AddVulnerabilities(&vuln)
	require.NoError(t, err)

	var results []VulnerabilityHandle
	err = db.Where("name = ?", "CVE-1234-5678").Preload("Provider").Find(&results).Error
	require.NoError(t, err)
	require.Len(t, results, 1, "expected exactly one vulnerability handle to be added")

	result := results[0]
	assert.NotEmpty(t, result.ProviderID)
	assert.NotEmpty(t, result.BlobID)
	if d := cmp.Diff(vuln, result, cmpopts.IgnoreFields(VulnerabilityHandle{}, "BlobValue")); d != "" {
		t.Errorf("unexpected result (-want +got):\n%s", d)
	}
}

func TestVulnerabilityStore_AddVulnerabilities_missingModifiedDate(t *testing.T) {
	db := setupTestStore(t).db
	bw := newBlobStore(db)
	s := newVulnerabilityStore(db, bw)

	now := time.Now()
	vuln := &VulnerabilityHandle{
		Name:          "CVE-1234-5678",
		PublishedDate: &now, // have publication date without modification date
		Provider: &Provider{
			ID: "provider!",
		},
	}

	err := s.AddVulnerabilities(vuln)
	require.NoError(t, err)

	// patched!
	assert.NotNil(t, vuln.ModifiedDate)
}

func TestVulnerabilityStore_AddVulnerabilities_Aliases(t *testing.T) {
	db := setupTestStore(t).db
	bw := newBlobStore(db)
	s := newVulnerabilityStore(db, bw)

	vuln := &VulnerabilityHandle{
		Name: "CVE-1234-5678",
		BlobValue: &VulnerabilityBlob{
			ID:      "CVE-1234-5678",
			Aliases: []string{"ALIAS-1", "ALIAS-2", "CVE-1234-5678"},
		},
		Provider: &Provider{
			ID: "provider!",
		},
	}

	err := s.AddVulnerabilities(vuln)
	require.NoError(t, err)

	var aliases []VulnerabilityAlias
	err = db.Where("name = ?", "CVE-1234-5678").Find(&aliases).Error
	require.NoError(t, err)

	expectedAliases := []VulnerabilityAlias{
		{Name: "CVE-1234-5678", Alias: "ALIAS-1"},
		{Name: "CVE-1234-5678", Alias: "ALIAS-2"},
	}
	assert.Len(t, aliases, len(expectedAliases))

	for _, expected := range expectedAliases {
		assert.Contains(t, aliases, expected)
	}

	uniqueAliases := make(map[string]struct{})
	for _, alias := range aliases {
		key := alias.Name + ":" + alias.Alias
		_, exists := uniqueAliases[key]
		assert.False(t, exists, "duplicate alias found")
		uniqueAliases[key] = struct{}{}
	}
}

func TestVulnerabilityStore_GetVulnerability_ByID(t *testing.T) {
	db := setupTestStore(t).db
	bw := newBlobStore(db)
	s := newVulnerabilityStore(db, bw)

	vuln := testVulnerabilityHandle()
	err := s.AddVulnerabilities(&vuln)
	require.NoError(t, err)

	results, err := s.GetVulnerabilities(&VulnerabilitySpecifier{ID: vuln.ID}, nil) // don't preload by default
	require.NoError(t, err)
	require.Len(t, results, 1)
	result := results[0]

	if d := cmp.Diff(vuln, result, cmpopts.IgnoreFields(VulnerabilityHandle{}, "Provider", "BlobValue")); d != "" {
		t.Errorf("unexpected result (-want +got):\n%s", d)
	}
	assert.Nil(t, result.BlobValue) // since we're not preloading any fields on the fetch
	assert.Nil(t, result.Provider)  // since we're not preloading any fields on the fetch

	results, err = s.GetVulnerabilities(&VulnerabilitySpecifier{ID: vuln.ID}, &GetVulnerabilityOptions{Preload: true})
	require.NoError(t, err)
	require.Len(t, results, 1)
	result = results[0]

	assert.NotNil(t, result.BlobValue)
	assert.NotNil(t, result.Provider)
	if d := cmp.Diff(vuln, result); d != "" {
		t.Errorf("unexpected result (-want +got):\n%s", d)
	}
}

func TestVulnerabilityStore_GetVulnerabilities_ByName(t *testing.T) {
	db := setupTestStore(t).db
	bw := newBlobStore(db)
	s := newVulnerabilityStore(db, bw)

	vuln1 := testVulnerabilityHandle()
	name := vuln1.Name
	vuln2 := VulnerabilityHandle{Name: name, BlobID: 2, Provider: vuln1.Provider, BlobValue: &VulnerabilityBlob{
		ID: name,
	}}
	err := s.AddVulnerabilities(&vuln1, &vuln2)
	require.NoError(t, err)

	expected := []VulnerabilityHandle{vuln1, vuln2}

	results, err := s.GetVulnerabilities(&VulnerabilitySpecifier{Name: name}, nil) // don't preload by default
	require.NoError(t, err)
	require.Len(t, results, 2)
	for i, result := range results {
		assert.Equal(t, expected[i].Name, result.Name)
		assert.Equal(t, expected[i].ID, result.ID)
		assert.Equal(t, expected[i].BlobID, result.BlobID)
		assert.Nil(t, result.BlobValue) // since we're not preloading any fields on the fetch
		assert.Nil(t, result.Provider)  // since we're not preloading any fields on the fetch
	}

	results, err = s.GetVulnerabilities(&VulnerabilitySpecifier{Name: name}, &GetVulnerabilityOptions{Preload: true})
	require.NoError(t, err)
	require.Len(t, results, 2)

	for i, result := range results {
		if d := cmp.Diff(expected[i], result); d != "" {
			t.Errorf("unexpected result (-want +got):\n%s", d)
		}
	}
}

func TestVulnerabilityStore_GetVulnerabilities_Aliases(t *testing.T) {
	db := setupTestStore(t).db
	bw := newBlobStore(db)
	s := newVulnerabilityStore(db, bw)

	vuln1 := &VulnerabilityHandle{
		Name: "CVE-1234-5678",
		BlobValue: &VulnerabilityBlob{
			ID:      "CVE-1234-5678",
			Aliases: []string{"ALIAS-1", "ALIAS-2"},
		},
		Provider: &Provider{
			ID: "provider!",
		},
	}

	vuln2 := &VulnerabilityHandle{
		Name: "ALIAS-1",
		BlobValue: &VulnerabilityBlob{
			ID: "ALIAS-1",
		},
		Provider: &Provider{
			ID: "provider2!",
		},
	}

	err := s.AddVulnerabilities(vuln1, vuln2)
	require.NoError(t, err)

	t.Run("include aliases", func(t *testing.T) {
		specifierWithAliases := &VulnerabilitySpecifier{
			Name:           "ALIAS-1",
			IncludeAliases: true,
		}

		results, err := s.GetVulnerabilities(specifierWithAliases, nil)
		require.NoError(t, err)
		require.Len(t, results, 2)
		assert.ElementsMatch(t, []string{"CVE-1234-5678", "ALIAS-1"}, []string{results[0].Name, results[1].Name})
	})

	t.Run("dont include aliases", func(t *testing.T) {
		specifierWithoutAliases := &VulnerabilitySpecifier{
			Name:           "ALIAS-1",
			IncludeAliases: false,
		}

		results, err := s.GetVulnerabilities(specifierWithoutAliases, nil)
		require.NoError(t, err)
		require.Len(t, results, 1)
		assert.Equal(t, "ALIAS-1", results[0].Name)
	})

	t.Run("direct match without aliases", func(t *testing.T) {
		specifierDirectMatch := &VulnerabilitySpecifier{
			Name:           "CVE-1234-5678",
			IncludeAliases: false,
		}

		results, err := s.GetVulnerabilities(specifierDirectMatch, nil)
		require.NoError(t, err)
		require.Len(t, results, 1)
		assert.Equal(t, "CVE-1234-5678", results[0].Name)
	})
}

func testVulnerabilityHandle() VulnerabilityHandle {
	now := time.Now()

	return VulnerabilityHandle{
		Name:          "CVE-8765-4321",
		Status:        "status!",
		PublishedDate: &now,
		ModifiedDate:  &now,
		WithdrawnDate: &now,
		Provider: &Provider{
			ID: "provider!",
		},
		BlobValue: &VulnerabilityBlob{
			ID:          "CVE-8765-4321",
			Assigners:   []string{"assigner!"},
			Description: "description!",
			References: []Reference{
				{
					URL:  "url!",
					Tags: []string{"tag!"},
				},
			},
			Aliases: []string{"alias!"},
			Severities: []Severity{
				{
					Scheme: "scheme!",
					Value:  "value!",
					Source: "source!",
					Rank:   10,
				},
				{
					Scheme: SeveritySchemeCVSS,
					Value: CVSSSeverity{
						Vector:  "CVSS:4.0/AV:L/AC:H/AT:P/PR:N/UI:P/VC:L/VI:H/VA:N/SC:N/SI:L/SA:N",
						Version: "4.0",
					},
				},
			},
		},
	}
}

func TestVulnerabilityStore_GetVulnerabilities_ByProviders(t *testing.T) {
	db := setupTestStore(t).db
	bw := newBlobStore(db)
	s := newVulnerabilityStore(db, bw)

	provider1 := &Provider{ID: "provider1"}
	provider2 := &Provider{ID: "provider2"}

	vuln1 := VulnerabilityHandle{Name: "CVE-1234-5678", BlobID: 1, Provider: provider1}
	vuln2 := VulnerabilityHandle{Name: "CVE-2345-6789", BlobID: 2, Provider: provider2}

	err := s.AddVulnerabilities(&vuln1, &vuln2)
	require.NoError(t, err)

	results, err := s.GetVulnerabilities(&VulnerabilitySpecifier{Providers: []string{"provider1"}}, nil)
	require.NoError(t, err)
	require.Len(t, results, 1)
	assert.Equal(t, vuln1.Name, results[0].Name)
	assert.Equal(t, vuln1.Provider.ID, results[0].ProviderID)

	results, err = s.GetVulnerabilities(&VulnerabilitySpecifier{Providers: []string{"provider1", "provider2"}}, nil)
	require.NoError(t, err)
	require.Len(t, results, 2)
	assert.ElementsMatch(t, []string{vuln1.Name, vuln2.Name}, []string{results[0].Name, results[1].Name})
}

func TestVulnerabilityStore_GetVulnerabilities_FilterByMultipleFactors(t *testing.T) {
	db := setupTestStore(t).db
	bw := newBlobStore(db)
	s := newVulnerabilityStore(db, bw)

	now := time.Now()
	oneDayAgo := now.Add(-24 * time.Hour)
	halfDayAgo := now.Add(-12 * time.Hour)
	tenDaysAgo := now.Add(-240 * time.Hour)

	provider1 := &Provider{ID: "provider1"}
	provider2 := &Provider{ID: "provider2"}

	vuln1 := VulnerabilityHandle{
		Name:          "CVE-1234-5678",
		BlobID:        1,
		Provider:      provider1,
		PublishedDate: &halfDayAgo,
	}

	vuln2 := VulnerabilityHandle{
		Name:          "CVE-2345-6789",
		BlobID:        2,
		Provider:      provider2, // filtered out due to provider
		PublishedDate: &now,
	}

	vuln3 := VulnerabilityHandle{
		Name:          "CVE-1234-5678",
		BlobID:        3,
		Provider:      provider1,
		PublishedDate: &tenDaysAgo, // filtered out due to date
	}

	err := s.AddVulnerabilities(&vuln1, &vuln2, &vuln3)
	require.NoError(t, err)

	results, err := s.GetVulnerabilities(&VulnerabilitySpecifier{
		Providers:      []string{"provider1"}, // filter by provider...
		PublishedAfter: &oneDayAgo,            // filter by date published...
	}, nil)
	require.NoError(t, err)
	require.Len(t, results, 1)
	assert.Equal(t, vuln1.Name, results[0].Name)
}
