package binutils

import (
	"bytes"
	"io"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"github.com/anchore/packageurl-go"
	"github.com/anchore/syft/syft/cpe"
	"github.com/anchore/syft/syft/file"
	"github.com/anchore/syft/syft/internal/unionreader"
	"github.com/anchore/syft/syft/source"
	"github.com/anchore/syft/syft/source/directorysource"
)

func Test_ClassifierCPEs(t *testing.T) {
	tests := []struct {
		name       string
		fixture    string
		classifier Classifier
		cpes       []string
	}{
		{
			name:    "no CPEs",
			fixture: "testdata/version.txt",
			classifier: Classifier{
				Package:         "some-app",
				FileGlob:        "**/version.txt",
				EvidenceMatcher: FileContentsVersionMatcher("cataloger-name", `(?m)my-version:(?P<version>[0-9.]+)`),
				CPEs:            []cpe.CPE{},
			},
			cpes: nil,
		},
		{
			name:    "one Attributes",
			fixture: "testdata/version.txt",
			classifier: Classifier{
				Package:         "some-app",
				FileGlob:        "**/version.txt",
				EvidenceMatcher: FileContentsVersionMatcher("cataloger-name", `(?m)my-version:(?P<version>[0-9.]+)`),
				CPEs: []cpe.CPE{
					cpe.Must("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*", cpe.GeneratedSource),
				},
			},
			cpes: []string{
				"cpe:2.3:a:some:app:1.8:*:*:*:*:*:*:*",
			},
		},
		{
			name:    "multiple CPEs",
			fixture: "testdata/version.txt",
			classifier: Classifier{
				Package:         "some-app",
				FileGlob:        "**/version.txt",
				EvidenceMatcher: FileContentsVersionMatcher("cataloger-name", `(?m)my-version:(?P<version>[0-9.]+)`),
				CPEs: []cpe.CPE{
					cpe.Must("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*", cpe.GeneratedSource),
					cpe.Must("cpe:2.3:a:some:apps:*:*:*:*:*:*:*:*", cpe.GeneratedSource),
				},
			},
			cpes: []string{
				"cpe:2.3:a:some:app:1.8:*:*:*:*:*:*:*",
				"cpe:2.3:a:some:apps:1.8:*:*:*:*:*:*:*",
			},
		},
		{
			name:    "version in parts",
			fixture: "testdata/version-parts.txt",
			classifier: Classifier{
				Package:         "some-app",
				FileGlob:        "**/version-parts.txt",
				EvidenceMatcher: FileContentsVersionMatcher("cataloger-name", `(?m)\x00(?P<major>[0-9.]+)\x00(?P<minor>[0-9.]+)\x00(?P<patch>[0-9.]+)\x00`),
				CPEs:            []cpe.CPE{},
			},
			cpes: nil,
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			resolver := file.NewMockResolverForPaths(test.fixture)
			ls, err := resolver.FilesByPath(test.fixture)
			require.NoError(t, err)
			require.Len(t, ls, 1)

			pkgs, err := test.classifier.EvidenceMatcher(test.classifier, MatcherContext{Resolver: resolver, Location: ls[0]})
			require.NoError(t, err)

			require.Len(t, pkgs, 1)

			p := pkgs[0]

			var cpes []string
			for _, c := range p.CPEs {
				cpes = append(cpes, c.Attributes.String())
			}
			require.Equal(t, test.cpes, cpes)
		})
	}
}

func TestClassifier_MarshalJSON(t *testing.T) {

	tests := []struct {
		name       string
		classifier Classifier
		want       string
		wantErr    assert.ErrorAssertionFunc
	}{
		{
			name: "go case",
			classifier: Classifier{
				Class:           "class",
				FileGlob:        "glob",
				EvidenceMatcher: FileContentsVersionMatcher("cataloger-name", ".thing"),
				Package:         "pkg",
				PURL: packageurl.PackageURL{
					Type:       "type",
					Namespace:  "namespace",
					Name:       "name",
					Version:    "version",
					Qualifiers: nil,
					Subpath:    "subpath",
				},
				CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*", cpe.GeneratedSource)},
			},
			want: `{"class":"class","fileGlob":"glob","package":"pkg","purl":"pkg:type/namespace/name@version#subpath","cpes":["cpe:2.3:a:some:app:*:*:*:*:*:*:*:*"]}`,
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if tt.wantErr == nil {
				tt.wantErr = assert.NoError
			}
			cfg := tt.classifier
			got, err := cfg.MarshalJSON()
			if !tt.wantErr(t, err) {
				return
			}
			assert.Equal(t, tt.want, string(got))
		})
	}
}

func TestFileContentsVersionMatcher(t *testing.T) {
	tests := []struct {
		name     string
		pattern  string
		data     string
		expected string
	}{
		{
			name:     "simple version string regexp",
			pattern:  `some data (?P<version>[0-9]+\.[0-9]+\.[0-9]+) some data`,
			data:     "some data 1.2.3 some data",
			expected: "1.2.3",
		},
		{
			name:     "version parts regexp",
			pattern:  `\x00\x23(?P<major>[0-9]+)\x00\x23(?P<minor>[0-9]+)\x00\x23(?P<patch>[0-9]+)\x00\x23`,
			data:     "\x00\x239\x00\x239\x00\x239\x00\x23",
			expected: "9.9.9",
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			mockGetContent := func(context MatcherContext) (unionreader.UnionReader, error) {
				return unionreader.GetUnionReader(io.NopCloser(bytes.NewBufferString(tt.data)))
			}
			fn := FileContentsVersionMatcher("cataloger-name", tt.pattern)
			p, err := fn(Classifier{}, MatcherContext{
				GetReader: mockGetContent,
			})

			if err != nil {
				t.Errorf("Unexpected error %#v", err)
			}

			if p[0].Version != tt.expected {
				t.Errorf("Versions don't match.\ngot\n%q\n\nexpected\n%q", p[0].Version, tt.expected)
			}
		})
	}
}

func Test_SupportingEvidenceMatcher(t *testing.T) {

	tests := []struct {
		name       string
		classifier Classifier
		expected   string
	}{
		{
			name: "simple version string regexp",
			classifier: Classifier{
				FileGlob: "**/some-binary",
				EvidenceMatcher: SupportingEvidenceMatcher("../version.txt",
					FileContentsVersionMatcher("cataloger-name", `(?m)my-version:(?P<version>[0-9.]+)`)),
				Package: "some-binary",
			},
			expected: "1.8",
		},
		{
			name: "not matching version string regexp",
			classifier: Classifier{
				FileGlob: "**/some-binary",
				EvidenceMatcher: SupportingEvidenceMatcher("../version.txt",
					FileContentsVersionMatcher("cataloger-name", `(?m)my-version:(?P<version>abdd)`)),
				Package: "some-binary",
			},
			expected: "",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s, err := directorysource.NewFromPath("testdata")
			require.NoError(t, err)
			r, err := s.FileResolver(source.AllLayersScope)
			require.NoError(t, err)

			results, err := r.FilesByGlob(tt.classifier.FileGlob)
			require.NoError(t, err)
			for _, result := range results {
				got, err := tt.classifier.EvidenceMatcher(tt.classifier, MatcherContext{
					Resolver: r,
					Location: result,
					GetReader: func(ctx MatcherContext) (unionreader.UnionReader, error) {
						return getReader(ctx)
					},
				})
				require.NoError(t, err)
				if tt.expected != "" {
					require.NotEmpty(t, got)
					require.Equal(t, tt.expected, got[0].Version)
				} else {
					require.Empty(t, got)
				}
			}
		})
	}
}

func TestMatchNone(t *testing.T) {
	matchingMatcher := MatchPath("**")
	notMatchingMatcher := MatchPath("will-not-match")

	tests := []struct {
		name     string
		matcher  EvidenceMatcher
		expected bool // true if MatchNone should succeed (inner failed)
	}{
		{
			name:     "inner matches, MatchNone fails",
			matcher:  MatchNone(matchingMatcher),
			expected: false,
		},
		{
			name:     "inner fails, MatchNone succeeds",
			matcher:  MatchNone(notMatchingMatcher),
			expected: true,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			pkgs, err := tt.matcher(Classifier{}, MatcherContext{
				Location: file.NewLocation("some/path"),
			})
			require.NoError(t, err)
			if tt.expected {
				assert.NotNil(t, pkgs)
				assert.Empty(t, pkgs)
			} else {
				assert.Nil(t, pkgs)
			}
		})
	}
}
