package redhat

import (
	"context"
	"errors"
	"testing"

	"github.com/google/go-cmp/cmp/cmpopts"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	_ "modernc.org/sqlite"

	"github.com/anchore/syft/syft/artifact"
	"github.com/anchore/syft/syft/file"
	"github.com/anchore/syft/syft/pkg"
	"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
)

func Test_DBCataloger(t *testing.T) {
	ctx := context.TODO()
	dbLocation := file.NewLocation("/var/lib/rpm/rpmdb.sqlite")
	locations := file.NewLocationSet(dbLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation))

	basePkg := pkg.Package{
		Name:      "basesystem",
		Version:   "11-13.el9",
		Type:      pkg.RpmPkg,
		Locations: locations,
		Licenses:  pkg.NewLicenseSet(pkg.NewLicenseFromLocationsWithContext(ctx, "Public Domain", dbLocation)),
		FoundBy:   "rpm-db-cataloger",
		PURL:      "pkg:rpm/basesystem@11-13.el9?arch=noarch&upstream=basesystem-11-13.el9.src.rpm",
		Metadata: pkg.RpmDBEntry{
			Name:       "basesystem",
			Version:    "11",
			Arch:       "noarch",
			Release:    "13.el9",
			SourceRpm:  "basesystem-11-13.el9.src.rpm",
			Size:       0,
			Vendor:     "Rocky Enterprise Software Foundation",
			Signatures: mustParseSignatures(t, "RSA/SHA256, Wed May 11 11:12:32 2022, Key ID 702d426d350d275d"),
			Provides:   []string{"basesystem"},
			Requires: []string{
				"filesystem",
				"rpmlib(CompressedFileNames)",
				"rpmlib(FileDigests)",
				"rpmlib(PayloadFilesHavePrefix)",
				"rpmlib(PayloadIsZstd)",
				"setup",
			},
			ModularityLabel: strRef(""),
		},
	}
	basePkg.SetID()

	bashPkg := pkg.Package{
		Name:      "bash",
		Version:   "5.1.8-6.el9_1",
		Type:      pkg.RpmPkg,
		Locations: locations,
		Licenses:  pkg.NewLicenseSet(pkg.NewLicenseFromLocationsWithContext(ctx, "GPLv3+", dbLocation)),
		FoundBy:   "rpm-db-cataloger",
		PURL:      "pkg:rpm/bash@5.1.8-6.el9_1?arch=x86_64&upstream=bash-5.1.8-6.el9_1.src.rpm",
		Metadata: pkg.RpmDBEntry{
			Name:            "bash",
			Version:         "5.1.8",
			Arch:            "x86_64",
			Release:         "6.el9_1",
			SourceRpm:       "bash-5.1.8-6.el9_1.src.rpm",
			Size:            7738634,
			Signatures:      mustParseSignatures(t, "RSA/SHA256, Mon Jan 23 22:49:22 2023, Key ID 702d426d350d275d"),
			ModularityLabel: strRef(""),
			Vendor:          "Rocky Enterprise Software Foundation",
			Provides: []string{
				"/bin/bash",
				"/bin/sh",
				"bash",
				"bash(x86-64)",
				"config(bash)",
			},
			Requires: []string{
				"/usr/bin/sh",
				"config(bash)",
				"filesystem",
				"libc.so.6()(64bit)",
				"libc.so.6(GLIBC_2.11)(64bit)",
				"libc.so.6(GLIBC_2.14)(64bit)",
				"libc.so.6(GLIBC_2.15)(64bit)",
				"libc.so.6(GLIBC_2.2.5)(64bit)",
				"libc.so.6(GLIBC_2.25)(64bit)",
				"libc.so.6(GLIBC_2.3)(64bit)",
				"libc.so.6(GLIBC_2.3.4)(64bit)",
				"libc.so.6(GLIBC_2.33)(64bit)",
				"libc.so.6(GLIBC_2.34)(64bit)",
				"libc.so.6(GLIBC_2.4)(64bit)",
				"libc.so.6(GLIBC_2.8)(64bit)",
				"libtinfo.so.6()(64bit)",
				"rpmlib(BuiltinLuaScripts)",
				"rpmlib(CompressedFileNames)",
				"rpmlib(FileDigests)",
				"rpmlib(PayloadFilesHavePrefix)",
				"rpmlib(PayloadIsZstd)",
				"rtld(GNU_HASH)",
			},
		},
	}
	bashPkg.SetID()

	filesystemPkg := pkg.Package{
		Name:      "filesystem",
		Version:   "3.16-2.el9",
		Type:      pkg.RpmPkg,
		Locations: locations,
		Licenses:  pkg.NewLicenseSet(pkg.NewLicenseFromLocationsWithContext(ctx, "Public Domain", dbLocation)),
		FoundBy:   "rpm-db-cataloger",
		PURL:      "pkg:rpm/filesystem@3.16-2.el9?arch=x86_64&upstream=filesystem-3.16-2.el9.src.rpm",
		Metadata: pkg.RpmDBEntry{
			Name:            "filesystem",
			Version:         "3.16",
			Arch:            "x86_64",
			Release:         "2.el9",
			SourceRpm:       "filesystem-3.16-2.el9.src.rpm",
			Size:            106,
			Signatures:      mustParseSignatures(t, "RSA/SHA256, Mon May 16 12:32:55 2022, Key ID 702d426d350d275d"),
			ModularityLabel: strRef(""),
			Vendor:          "Rocky Enterprise Software Foundation",
			Provides: []string{
				"filesystem",
				"filesystem(x86-64)",
				"filesystem-afs",
			},
			Requires: []string{
				"/bin/sh",
				"rpmlib(BuiltinLuaScripts)",
				"rpmlib(CompressedFileNames)",
				"rpmlib(FileDigests)",
				"rpmlib(PayloadFilesHavePrefix)",
				"rpmlib(PayloadIsZstd)",
				"setup",
			},
		},
	}
	filesystemPkg.SetID()

	expectedPackages := []pkg.Package{basePkg, bashPkg, filesystemPkg}

	// Note that you'll see a cycle:
	//   bash --(requires)--> filesystem
	//   filesystem --(requires)--> bash
	//
	// This is not a bug!
	//
	// [root@c1a4773e8a8d /]# dnf repoquery --requires --resolve filesystem
	//   bash-0:5.1.8-9.el9.aarch64
	//   setup-0:2.13.7-10.el9.noarch
	//
	//[root@c1a4773e8a8d /]# dnf repoquery --requires --resolve bash
	//  filesystem-0:3.16-2.el9.aarch64
	//  glibc-0:2.34-100.el9.aarch64
	//  ncurses-libs-0:6.2-10.20210508.el9.aarch64

	expectedRelationships := []artifact.Relationship{
		// though this is expressible in the RPM DB (package depends on itself), we do not allow for it in the SBOM
		//{
		//	From: bashPkg,
		//	To:   bashPkg,
		//	Type: artifact.DependencyOfRelationship,
		//},
		{
			From: bashPkg,
			To:   filesystemPkg,
			Type: artifact.DependencyOfRelationship,
		},
		{
			From: filesystemPkg,
			To:   basePkg,
			Type: artifact.DependencyOfRelationship,
		},
		{
			From: filesystemPkg,
			To:   bashPkg,
			Type: artifact.DependencyOfRelationship,
		},
	}

	pkgtest.NewCatalogTester().
		WithImageResolver(t, "image-minimal").
		IgnoreLocationLayer().                                               // this fixture can be rebuilt, thus the layer ID will change
		WithCompareOptions(cmpopts.IgnoreFields(pkg.RpmDBEntry{}, "Files")). // this is rather long... ano not the point of the test
		Expects(expectedPackages, expectedRelationships).
		TestCataloger(t, NewDBCataloger())

}

func Test_DBCataloger_Globs(t *testing.T) {
	tests := []struct {
		name     string
		fixture  string
		expected []string
	}{
		{
			name:    "obtain DB files",
			fixture: "testdata/glob-paths",
			expected: []string{
				"usr/share/rpm/Packages",
				"usr/share/rpm/Packages.db",
				"usr/share/rpm/rpmdb.sqlite",
				"var/lib/rpm/Packages",
				"var/lib/rpm/Packages.db",
				"var/lib/rpm/rpmdb.sqlite",
				"var/lib/rpmmanifest/container-manifest-2",
				"usr/lib/sysimage/rpm/Packages",
				"usr/lib/sysimage/rpm/Packages.db",
				"usr/lib/sysimage/rpm/rpmdb.sqlite",
			},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			pkgtest.NewCatalogTester().
				FromDirectory(t, test.fixture).
				ExpectsResolverContentQueries(test.expected).
				TestCataloger(t, NewDBCataloger())
		})
	}
}

func Test_RPMFileCataloger_Globs(t *testing.T) {
	tests := []struct {
		name     string
		fixture  string
		expected []string
	}{
		{
			name:    "obtain rpm files",
			fixture: "testdata/glob-paths",
			expected: []string{
				"dive-0.10.0.rpm",
			},
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			pkgtest.NewCatalogTester().
				FromDirectory(t, test.fixture).
				ExpectsResolverContentQueries(test.expected).
				TestCataloger(t, NewArchiveCataloger())
		})
	}
}

func Test_denySelfReferences(t *testing.T) {
	a := pkg.Package{
		Name: "a",
	}
	a.SetID()
	b := pkg.Package{
		Name: "b",
	}
	b.SetID()
	c := pkg.Package{
		Name: "c",
	}
	c.SetID()

	pkgs := []pkg.Package{a, b, c}

	tests := []struct {
		name              string
		pkgs              []pkg.Package
		rels              []artifact.Relationship
		err               error
		wantPkgs          int
		wantRelationships int
		wantErr           assert.ErrorAssertionFunc
	}{
		{
			name: "no self references",
			pkgs: pkgs,
			rels: []artifact.Relationship{
				{
					From: a,
					To:   b,
					Type: artifact.DependencyOfRelationship,
				},
			},
			wantPkgs:          3,
			wantRelationships: 1,
			wantErr:           assert.NoError,
		},
		{
			name: "remove self references",
			pkgs: pkgs,
			rels: []artifact.Relationship{
				{
					From: a,
					To:   a,
					Type: artifact.DependencyOfRelationship,
				},
				{
					From: a,
					To:   b,
					Type: artifact.DependencyOfRelationship,
				},
			},
			wantPkgs:          3,
			wantRelationships: 1,
			wantErr:           assert.NoError,
		},
		{
			name: "preserve errors",
			pkgs: pkgs,
			rels: []artifact.Relationship{
				{
					From: a,
					To:   b,
					Type: artifact.DependencyOfRelationship,
				},
			},
			err:               errors.New("stop me!"),
			wantPkgs:          3,
			wantRelationships: 1,
			wantErr:           assert.Error,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if tt.wantErr == nil {
				tt.wantErr = assert.NoError
			}

			gotPkgs, gotRels, err := denySelfReferences(tt.pkgs, tt.rels, tt.err)

			tt.wantErr(t, err)
			assert.Len(t, gotPkgs, tt.wantPkgs)
			assert.Len(t, gotRels, tt.wantRelationships)
		})
	}
}

func mustParseSignatures(t testing.TB, sigs ...string) []pkg.RpmSignature {
	signatures, err := parseSignatures(sigs...)
	require.NoError(t, err)
	return signatures
}
