// Copyright 2015 clair authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package pgsql

import (
	"reflect"
	"testing"

	"github.com/stretchr/testify/assert"

	"github.com/coreos/clair/database"
	cerrors "github.com/coreos/clair/utils/errors"
	"github.com/coreos/clair/utils/types"
)

func TestFindVulnerability(t *testing.T) {
	datastore, err := openDatabaseForTest("FindVulnerability", true)
	if err != nil {
		t.Error(err)
		return
	}
	defer datastore.Close()

	// Find a vulnerability that does not exist.
	_, err = datastore.FindVulnerability("", "")
	assert.Equal(t, cerrors.ErrNotFound, err)

	// Find a normal vulnerability.
	v1 := database.Vulnerability{
		Name:        "CVE-OPENSSL-1-DEB7",
		Description: "A vulnerability affecting OpenSSL < 2.0 on Debian 7.0",
		Link:        "http://google.com/#q=CVE-OPENSSL-1-DEB7",
		Severity:    types.High,
		Namespace:   database.Namespace{Name: "debian:7"},
		FixedIn: []database.FeatureVersion{
			{
				Feature: database.Feature{Name: "openssl"},
				Version: types.NewVersionUnsafe("2.0"),
			},
			{
				Feature: database.Feature{Name: "libssl"},
				Version: types.NewVersionUnsafe("1.9-abc"),
			},
		},
	}

	v1f, err := datastore.FindVulnerability("debian:7", "CVE-OPENSSL-1-DEB7")
	if assert.Nil(t, err) {
		equalsVuln(t, &v1, &v1f)
	}

	// Find a vulnerability that has no link, no severity and no FixedIn.
	v2 := database.Vulnerability{
		Name:        "CVE-NOPE",
		Description: "A vulnerability affecting nothing",
		Namespace:   database.Namespace{Name: "debian:7"},
		Severity:    types.Unknown,
	}

	v2f, err := datastore.FindVulnerability("debian:7", "CVE-NOPE")
	if assert.Nil(t, err) {
		equalsVuln(t, &v2, &v2f)
	}
}

func TestDeleteVulnerability(t *testing.T) {
	datastore, err := openDatabaseForTest("InsertVulnerability", true)
	if err != nil {
		t.Error(err)
		return
	}
	defer datastore.Close()

	// Delete non-existing Vulnerability.
	err = datastore.DeleteVulnerability("TestDeleteVulnerabilityNamespace1", "CVE-OPENSSL-1-DEB7")
	assert.Equal(t, cerrors.ErrNotFound, err)
	err = datastore.DeleteVulnerability("debian:7", "TestDeleteVulnerabilityVulnerability1")
	assert.Equal(t, cerrors.ErrNotFound, err)

	// Delete Vulnerability.
	err = datastore.DeleteVulnerability("debian:7", "CVE-OPENSSL-1-DEB7")
	if assert.Nil(t, err) {
		_, err := datastore.FindVulnerability("debian:7", "CVE-OPENSSL-1-DEB7")
		assert.Equal(t, cerrors.ErrNotFound, err)
	}
}

func TestInsertVulnerability(t *testing.T) {
	datastore, err := openDatabaseForTest("InsertVulnerability", false)
	if err != nil {
		t.Error(err)
		return
	}
	defer datastore.Close()

	// Create some data.
	n1 := database.Namespace{Name: "TestInsertVulnerabilityNamespace1"}
	n2 := database.Namespace{Name: "TestInsertVulnerabilityNamespace2"}

	f1 := database.FeatureVersion{
		Feature: database.Feature{
			Name:      "TestInsertVulnerabilityFeatureVersion1",
			Namespace: n1,
		},
		Version: types.NewVersionUnsafe("1.0"),
	}
	f2 := database.FeatureVersion{
		Feature: database.Feature{
			Name:      "TestInsertVulnerabilityFeatureVersion1",
			Namespace: n2,
		},
		Version: types.NewVersionUnsafe("1.0"),
	}
	f3 := database.FeatureVersion{
		Feature: database.Feature{
			Name: "TestInsertVulnerabilityFeatureVersion2",
		},
		Version: types.MaxVersion,
	}
	f4 := database.FeatureVersion{
		Feature: database.Feature{
			Name: "TestInsertVulnerabilityFeatureVersion2",
		},
		Version: types.NewVersionUnsafe("1.4"),
	}
	f5 := database.FeatureVersion{
		Feature: database.Feature{
			Name: "TestInsertVulnerabilityFeatureVersion3",
		},
		Version: types.NewVersionUnsafe("1.5"),
	}
	f6 := database.FeatureVersion{
		Feature: database.Feature{
			Name: "TestInsertVulnerabilityFeatureVersion4",
		},
		Version: types.NewVersionUnsafe("0.1"),
	}
	f7 := database.FeatureVersion{
		Feature: database.Feature{
			Name: "TestInsertVulnerabilityFeatureVersion5",
		},
		Version: types.MaxVersion,
	}
	f8 := database.FeatureVersion{
		Feature: database.Feature{
			Name: "TestInsertVulnerabilityFeatureVersion5",
		},
		Version: types.MinVersion,
	}

	// Insert invalid vulnerabilities.
	for _, vulnerability := range []database.Vulnerability{
		{
			Name:      "",
			Namespace: n1,
			FixedIn:   []database.FeatureVersion{f1},
			Severity:  types.Unknown,
		},
		{
			Name:      "TestInsertVulnerability0",
			Namespace: database.Namespace{},
			FixedIn:   []database.FeatureVersion{f1},
			Severity:  types.Unknown,
		},
		{
			Name:      "TestInsertVulnerability0-",
			Namespace: database.Namespace{},
			FixedIn:   []database.FeatureVersion{f1},
		},
		{
			Name:      "TestInsertVulnerability0",
			Namespace: n1,
			FixedIn:   []database.FeatureVersion{f1},
			Severity:  types.Priority(""),
		},
		{
			Name:      "TestInsertVulnerability0",
			Namespace: n1,
			FixedIn:   []database.FeatureVersion{f2},
			Severity:  types.Unknown,
		},
	} {
		err := datastore.InsertVulnerabilities([]database.Vulnerability{vulnerability}, true)
		assert.Error(t, err)
	}

	// Insert a simple vulnerability and find it.
	v1meta := make(map[string]interface{})
	v1meta["TestInsertVulnerabilityMetadata1"] = "TestInsertVulnerabilityMetadataValue1"
	v1meta["TestInsertVulnerabilityMetadata2"] = struct {
		Test string
	}{
		Test: "TestInsertVulnerabilityMetadataValue1",
	}

	v1 := database.Vulnerability{
		Name:        "TestInsertVulnerability1",
		Namespace:   n1,
		FixedIn:     []database.FeatureVersion{f1, f3, f6, f7},
		Severity:    types.Low,
		Description: "TestInsertVulnerabilityDescription1",
		Link:        "TestInsertVulnerabilityLink1",
		Metadata:    v1meta,
	}
	err = datastore.InsertVulnerabilities([]database.Vulnerability{v1}, true)
	if assert.Nil(t, err) {
		v1f, err := datastore.FindVulnerability(n1.Name, v1.Name)
		if assert.Nil(t, err) {
			equalsVuln(t, &v1, &v1f)
		}
	}

	// Update vulnerability.
	v1.Description = "TestInsertVulnerabilityLink2"
	v1.Link = "TestInsertVulnerabilityLink2"
	v1.Severity = types.High
	// Update f3 in f4, add fixed in f5, add fixed in f6 which already exists,
	// removes fixed in f7 by adding f8 which is f7 but with MinVersion, and
	// add fixed by f5 a second time (duplicated).
	v1.FixedIn = []database.FeatureVersion{f4, f5, f6, f8, f5}

	err = datastore.InsertVulnerabilities([]database.Vulnerability{v1}, true)
	if assert.Nil(t, err) {
		v1f, err := datastore.FindVulnerability(n1.Name, v1.Name)
		if assert.Nil(t, err) {
			// Remove f8 from the struct for comparison as it was just here to cancel f7.
			// Remove one of the f5 too as it was twice in the struct but the database
			// implementation should have dedup'd it.
			v1.FixedIn = v1.FixedIn[:len(v1.FixedIn)-2]

			// We already had f1 before the update.
			// Add it to the struct for comparison.
			v1.FixedIn = append(v1.FixedIn, f1)

			equalsVuln(t, &v1, &v1f)
		}
	}
}

func equalsVuln(t *testing.T, expected, actual *database.Vulnerability) {
	assert.Equal(t, expected.Name, actual.Name)
	assert.Equal(t, expected.Namespace.Name, actual.Namespace.Name)
	assert.Equal(t, expected.Description, actual.Description)
	assert.Equal(t, expected.Link, actual.Link)
	assert.Equal(t, expected.Severity, actual.Severity)
	assert.True(t, reflect.DeepEqual(castMetadata(expected.Metadata), actual.Metadata), "Got metadata %#v, expected %#v", actual.Metadata, castMetadata(expected.Metadata))

	if assert.Len(t, actual.FixedIn, len(expected.FixedIn)) {
		for _, actualFeatureVersion := range actual.FixedIn {
			found := false
			for _, expectedFeatureVersion := range expected.FixedIn {
				if expectedFeatureVersion.Feature.Name == actualFeatureVersion.Feature.Name {
					found = true

					assert.Equal(t, expected.Namespace.Name, actualFeatureVersion.Feature.Namespace.Name)
					assert.Equal(t, expectedFeatureVersion.Version, actualFeatureVersion.Version)
				}
			}
			if !found {
				t.Errorf("unexpected package %s in %s", actualFeatureVersion.Feature.Name, expected.Name)
			}
		}
	}
}
