package packagemanager

import (
	"context"
	"testing"

	packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
	"github.com/safedep/dry/semver"
	"github.com/stretchr/testify/require"
)

func TestNpmDependencyResolver_ResolveLatestVersion(t *testing.T) {
	cases := []struct {
		name     string
		pkg      *packagev1.Package
		assertFn func(t *testing.T, pv *packagev1.PackageVersion, err error)
	}{
		{
			name: "should resolve latest version for a package",
			pkg: &packagev1.Package{
				Name:      "react",
				Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
			},
			assertFn: func(t *testing.T, pv *packagev1.PackageVersion, err error) {
				require.NoError(t, err)
				require.True(t, semver.IsAhead("19.0.0", pv.Version))
			},
		},
		{
			name: "should return an error if the package is not found",
			pkg: &packagev1.Package{
				Name:      "nonexistent",
				Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
			},
			assertFn: func(t *testing.T, pv *packagev1.PackageVersion, err error) {
				require.Error(t, err)
				require.Nil(t, pv)
			},
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			resolver, err := NewNpmDependencyResolver(NewDefaultNpmDependencyResolverConfig())
			require.NoError(t, err)

			pv, err := resolver.ResolveLatestVersion(context.Background(), tc.pkg)
			tc.assertFn(t, pv, err)
		})
	}
}

func TestNpmDependencyResolver_ResolveDependencies(t *testing.T) {
	cases := []struct {
		name                          string
		pkg                           *packagev1.PackageVersion
		includeTransitiveDependencies bool
		transitiveDepth               int
		failFast                      bool
		assertFn                      func(t *testing.T, dependencies []*packagev1.PackageVersion, err error)
	}{
		{
			name: "should resolve dependencies for a package when transitive dependencies are not included",
			pkg: &packagev1.PackageVersion{
				Package: &packagev1.Package{
					Name:      "react",
					Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
				},
				Version: "18.2.0",
			},
			includeTransitiveDependencies: false,
			transitiveDepth:               1,
			assertFn: func(t *testing.T, dependencies []*packagev1.PackageVersion, err error) {
				require.NoError(t, err)
				require.Equal(t, 1, len(dependencies))
				require.Equal(t, "loose-envify", dependencies[0].Package.Name)
				require.Equal(t, "1.1.0", dependencies[0].Version)
			},
		},
		{
			name: "should resolve dependencies for a package up to a given depth",
			pkg: &packagev1.PackageVersion{
				Package: &packagev1.Package{
					Name:      "react",
					Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
				},
				Version: "18.2.0",
			},
			includeTransitiveDependencies: true,
			transitiveDepth:               2,
			assertFn: func(t *testing.T, dependencies []*packagev1.PackageVersion, err error) {
				require.NoError(t, err)
				require.Equal(t, 2, len(dependencies))

				packageNames := []string{}
				for _, dep := range dependencies {
					packageNames = append(packageNames, dep.Package.Name)
				}

				require.ElementsMatch(t, []string{
					"loose-envify",
					"js-tokens",
				}, packageNames)
			},
		},
		{
			name: "should resolve all dependencies for a package when transitive dependencies are included",
			pkg: &packagev1.PackageVersion{
				Package: &packagev1.Package{
					Name:      "express",
					Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
				},
				Version: "4.18.2",
			},
			includeTransitiveDependencies: true,
			transitiveDepth:               5,
			assertFn: func(t *testing.T, dependencies []*packagev1.PackageVersion, err error) {
				require.NoError(t, err)
				require.Greater(t, len(dependencies), 5, "Express should have more than 5 dependencies")
			},
		},
		{
			name: "should not fail when package is not found without fail fast",
			pkg: &packagev1.PackageVersion{
				Package: &packagev1.Package{
					Name:      "nonexistent",
					Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
				},
				Version: "1.0.0",
			},
			assertFn: func(t *testing.T, dependencies []*packagev1.PackageVersion, err error) {
				require.NoError(t, err)
				require.Empty(t, dependencies)
			},
		},
		{
			name: "should fail when package is not found with fail fast",
			pkg: &packagev1.PackageVersion{
				Package: &packagev1.Package{
					Name:      "nonexistent",
					Ecosystem: packagev1.Ecosystem_ECOSYSTEM_NPM,
				},
				Version: "1.0.0",
			},
			failFast: true,
			assertFn: func(t *testing.T, dependencies []*packagev1.PackageVersion, err error) {
				require.Error(t, err)
				require.Nil(t, dependencies)
			},
		},
	}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			config := NewDefaultNpmDependencyResolverConfig()
			config.IncludeTransitiveDependencies = tc.includeTransitiveDependencies
			config.TransitiveDepth = tc.transitiveDepth
			config.FailFast = tc.failFast

			resolver, err := NewNpmDependencyResolver(config)
			require.NoError(t, err)

			dependencies, err := resolver.ResolveDependencies(context.Background(), tc.pkg)
			tc.assertFn(t, dependencies, err)
		})
	}
}
