// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package getproviders

import (
	"context"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/opentofu/opentofu/internal/addrs"
)

func TestMultiSourceAvailableVersions(t *testing.T) {
	platform1 := Platform{OS: "amigaos", Arch: "m68k"}
	platform2 := Platform{OS: "aros", Arch: "arm"}

	t.Run("unfiltered merging", func(t *testing.T) {
		s1 := NewMockSource([]PackageMeta{
			FakePackageMeta(
				addrs.NewDefaultProvider("foo"),
				MustParseVersion("1.0.0"),
				VersionList{MustParseVersion("5.0")},
				platform1,
			),
			FakePackageMeta(
				addrs.NewDefaultProvider("foo"),
				MustParseVersion("1.0.0"),
				VersionList{MustParseVersion("5.0")},
				platform2,
			),
			FakePackageMeta(
				addrs.NewDefaultProvider("bar"),
				MustParseVersion("1.0.0"),
				VersionList{MustParseVersion("5.0")},
				platform2,
			),
		},
			nil,
		)
		s2 := NewMockSource([]PackageMeta{
			FakePackageMeta(
				addrs.NewDefaultProvider("foo"),
				MustParseVersion("1.0.0"),
				VersionList{MustParseVersion("5.0")},
				platform1,
			),
			FakePackageMeta(
				addrs.NewDefaultProvider("foo"),
				MustParseVersion("1.2.0"),
				VersionList{MustParseVersion("5.0")},
				platform1,
			),
			FakePackageMeta(
				addrs.NewDefaultProvider("bar"),
				MustParseVersion("1.0.0"),
				VersionList{MustParseVersion("5.0")},
				platform1,
			),
		},
			nil,
		)
		multi := MultiSource{
			{Source: s1},
			{Source: s2},
		}

		// AvailableVersions produces the union of all versions available
		// across all of the sources.
		got, _, err := multi.AvailableVersions(context.Background(), addrs.NewDefaultProvider("foo"))
		if err != nil {
			t.Fatalf("unexpected error: %s", err)
		}
		want := VersionList{
			MustParseVersion("1.0.0"),
			MustParseVersion("1.2.0"),
		}

		if diff := cmp.Diff(want, got); diff != "" {
			t.Errorf("wrong result\n%s", diff)
		}

		_, _, err = multi.AvailableVersions(context.Background(), addrs.NewDefaultProvider("baz"))
		if want, ok := err.(ErrRegistryProviderNotKnown); !ok {
			t.Fatalf("wrong error type:\ngot:  %T\nwant: %T", err, want)
		}
	})

	t.Run("merging with filters", func(t *testing.T) {
		// This is just testing that filters are being honored at all, using a
		// specific pair of filters. The different filter combinations
		// themselves are tested in TestMultiSourceSelector.

		s1 := NewMockSource([]PackageMeta{
			FakePackageMeta(
				addrs.NewDefaultProvider("foo"),
				MustParseVersion("1.0.0"),
				VersionList{MustParseVersion("5.0")},
				platform1,
			),
			FakePackageMeta(
				addrs.NewDefaultProvider("bar"),
				MustParseVersion("1.0.0"),
				VersionList{MustParseVersion("5.0")},
				platform1,
			),
		},
			nil,
		)
		s2 := NewMockSource([]PackageMeta{
			FakePackageMeta(
				addrs.NewDefaultProvider("foo"),
				MustParseVersion("1.2.0"),
				VersionList{MustParseVersion("5.0")},
				platform1,
			),
			FakePackageMeta(
				addrs.NewDefaultProvider("bar"),
				MustParseVersion("1.2.0"),
				VersionList{MustParseVersion("5.0")},
				platform1,
			),
		},
			nil,
		)
		multi := MultiSource{
			{
				Source:  s1,
				Include: mustParseMultiSourceMatchingPatterns("hashicorp/*"),
			},
			{
				Source:  s2,
				Include: mustParseMultiSourceMatchingPatterns("hashicorp/bar"),
			},
		}

		got, _, err := multi.AvailableVersions(context.Background(), addrs.NewDefaultProvider("foo"))
		if err != nil {
			t.Fatalf("unexpected error: %s", err)
		}
		want := VersionList{
			MustParseVersion("1.0.0"),
			// 1.2.0 isn't present because s3 doesn't include "foo"
		}
		if diff := cmp.Diff(want, got); diff != "" {
			t.Errorf("wrong result\n%s", diff)
		}

		got, _, err = multi.AvailableVersions(context.Background(), addrs.NewDefaultProvider("bar"))
		if err != nil {
			t.Fatalf("unexpected error: %s", err)
		}
		want = VersionList{
			MustParseVersion("1.0.0"),
			MustParseVersion("1.2.0"), // included because s2 matches "bar"
		}
		if diff := cmp.Diff(want, got); diff != "" {
			t.Errorf("wrong result\n%s", diff)
		}

		_, _, err = multi.AvailableVersions(context.Background(), addrs.NewDefaultProvider("baz"))
		if want, ok := err.(ErrRegistryProviderNotKnown); !ok {
			t.Fatalf("wrong error type:\ngot:  %T\nwant: %T", err, want)
		}
	})

	t.Run("provider not found", func(t *testing.T) {
		s1 := NewMockSource(nil, nil)
		s2 := NewMockSource(nil, nil)
		multi := MultiSource{
			{Source: s1},
			{Source: s2},
		}

		_, _, err := multi.AvailableVersions(context.Background(), addrs.NewDefaultProvider("foo"))
		if err == nil {
			t.Fatal("expected error, got success")
		}

		wantErr := `provider registry registry.opentofu.org does not have a provider named registry.opentofu.org/hashicorp/foo`

		if err.Error() != wantErr {
			t.Fatalf("wrong error.\ngot:  %s\nwant: %s\n", err, wantErr)
		}

	})

	t.Run("merging with warnings", func(t *testing.T) {
		platform1 := Platform{OS: "amigaos", Arch: "m68k"}
		platform2 := Platform{OS: "aros", Arch: "arm"}
		s1 := NewMockSource([]PackageMeta{
			FakePackageMeta(
				addrs.NewDefaultProvider("bar"),
				MustParseVersion("1.0.0"),
				VersionList{MustParseVersion("5.0")},
				platform2,
			),
		},
			map[addrs.Provider]Warnings{
				addrs.NewDefaultProvider("bar"): {"WARNING!"},
			},
		)
		s2 := NewMockSource([]PackageMeta{
			FakePackageMeta(
				addrs.NewDefaultProvider("bar"),
				MustParseVersion("1.0.0"),
				VersionList{MustParseVersion("5.0")},
				platform1,
			),
		},
			nil,
		)
		multi := MultiSource{
			{Source: s1},
			{Source: s2},
		}

		// AvailableVersions produces the union of all versions available
		// across all of the sources.
		got, warns, err := multi.AvailableVersions(context.Background(), addrs.NewDefaultProvider("bar"))
		if err != nil {
			t.Fatalf("unexpected error: %s", err)
		}
		want := VersionList{
			MustParseVersion("1.0.0"),
		}
		if diff := cmp.Diff(want, got); diff != "" {
			t.Errorf("wrong result\n%s", diff)
		}

		if len(warns) != 1 {
			t.Fatalf("wrong number of warnings. Got %d, wanted 1", len(warns))
		}
		if warns[0] != "WARNING!" {
			t.Fatalf("wrong warnings. Got %s, wanted \"WARNING!\"", warns[0])
		}
	})
}

func TestMultiSourcePackageMeta(t *testing.T) {
	platform1 := Platform{OS: "amigaos", Arch: "m68k"}
	platform2 := Platform{OS: "aros", Arch: "arm"}

	// We'll use the Filename field of the fake PackageMetas we created above
	// to create a difference between the packages in s1 and the ones in s2,
	// so we can test where individual packages came from below.
	fakeFilename := func(fn string, meta PackageMeta) PackageMeta {
		meta.Filename = fn
		return meta
	}

	onlyInS1 := fakeFilename("s1", FakePackageMeta(
		addrs.NewDefaultProvider("foo"),
		MustParseVersion("1.0.0"),
		VersionList{MustParseVersion("5.0")},
		platform2,
	))
	onlyInS2 := fakeFilename("s2", FakePackageMeta(
		addrs.NewDefaultProvider("foo"),
		MustParseVersion("1.2.0"),
		VersionList{MustParseVersion("5.0")},
		platform1,
	))
	inBothS1 := fakeFilename("s1", FakePackageMeta(
		addrs.NewDefaultProvider("foo"),
		MustParseVersion("1.0.0"),
		VersionList{MustParseVersion("5.0")},
		platform1,
	))
	inBothS2 := fakeFilename("s2", inBothS1)
	s1 := NewMockSource([]PackageMeta{
		inBothS1,
		onlyInS1,
		fakeFilename("s1", FakePackageMeta(
			addrs.NewDefaultProvider("bar"),
			MustParseVersion("1.0.0"),
			VersionList{MustParseVersion("5.0")},
			platform2,
		)),
	},
		nil,
	)
	s2 := NewMockSource([]PackageMeta{
		inBothS2,
		onlyInS2,
		fakeFilename("s2", FakePackageMeta(
			addrs.NewDefaultProvider("bar"),
			MustParseVersion("1.0.0"),
			VersionList{MustParseVersion("5.0")},
			platform1,
		)),
	}, nil)
	multi := MultiSource{
		{Source: s1},
		{Source: s2},
	}

	t.Run("only in s1", func(t *testing.T) {
		got, err := multi.PackageMeta(
			context.Background(),
			addrs.NewDefaultProvider("foo"),
			MustParseVersion("1.0.0"),
			platform2,
		)
		want := onlyInS1
		if err != nil {
			t.Fatalf("unexpected error: %s", err)
		}
		if diff := cmp.Diff(want, got); diff != "" {
			t.Errorf("wrong result\n%s", diff)
		}
	})
	t.Run("only in s2", func(t *testing.T) {
		got, err := multi.PackageMeta(
			context.Background(),
			addrs.NewDefaultProvider("foo"),
			MustParseVersion("1.2.0"),
			platform1,
		)
		want := onlyInS2
		if err != nil {
			t.Fatalf("unexpected error: %s", err)
		}
		if diff := cmp.Diff(want, got); diff != "" {
			t.Errorf("wrong result\n%s", diff)
		}
	})
	t.Run("in both", func(t *testing.T) {
		got, err := multi.PackageMeta(
			context.Background(),
			addrs.NewDefaultProvider("foo"),
			MustParseVersion("1.0.0"),
			platform1,
		)
		want := inBothS1 // S1 "wins" because it's earlier in the MultiSource
		if err != nil {
			t.Fatalf("unexpected error: %s", err)
		}
		if diff := cmp.Diff(want, got); diff != "" {
			t.Errorf("wrong result\n%s", diff)
		}

		// Make sure inBothS1 and inBothS2 really are different; if not then
		// that's a test bug which we'd rather catch than have this test
		// accidentally passing without actually checking anything.
		if diff := cmp.Diff(inBothS1, inBothS2); diff == "" {
			t.Fatalf("test bug: inBothS1 and inBothS2 are indistinguishable")
		}
	})
	t.Run("in neither", func(t *testing.T) {
		_, err := multi.PackageMeta(
			context.Background(),
			addrs.NewDefaultProvider("nonexist"),
			MustParseVersion("1.0.0"),
			platform1,
		)
		// This case reports "platform not supported" because it assumes that
		// a caller would only pass to it package versions that were returned
		// by a previous call to AvailableVersions, and therefore a missing
		// object ought to be valid provider/version but an unsupported
		// platform.
		if want, ok := err.(ErrPlatformNotSupported); !ok {
			t.Fatalf("wrong error type:\ngot:  %T\nwant: %T", err, want)
		}
	})
}

func TestMultiSourceSelector(t *testing.T) {
	emptySource := NewMockSource(nil, nil)

	tests := map[string]struct {
		Selector  MultiSourceSelector
		Provider  addrs.Provider
		WantMatch bool
	}{
		"default provider with no constraints": {
			MultiSourceSelector{
				Source: emptySource,
			},
			addrs.NewDefaultProvider("foo"),
			true,
		},
		"built-in provider with no constraints": {
			MultiSourceSelector{
				Source: emptySource,
			},
			addrs.NewBuiltInProvider("bar"),
			true,
		},

		// Include constraints
		"default provider with include constraint that matches it exactly": {
			MultiSourceSelector{
				Source:  emptySource,
				Include: mustParseMultiSourceMatchingPatterns("hashicorp/foo"),
			},
			addrs.NewDefaultProvider("foo"),
			true,
		},
		"default provider with include constraint that matches it via type wildcard": {
			MultiSourceSelector{
				Source:  emptySource,
				Include: mustParseMultiSourceMatchingPatterns("hashicorp/*"),
			},
			addrs.NewDefaultProvider("foo"),
			true,
		},
		"default provider with include constraint that matches it via namespace wildcard": {
			MultiSourceSelector{
				Source:  emptySource,
				Include: mustParseMultiSourceMatchingPatterns("*/*"),
			},
			addrs.NewDefaultProvider("foo"),
			true,
		},
		"default provider with non-normalized include constraint that matches it via type wildcard": {
			MultiSourceSelector{
				Source:  emptySource,
				Include: mustParseMultiSourceMatchingPatterns("HashiCorp/*"),
			},
			addrs.NewDefaultProvider("foo"),
			true,
		},
		"built-in provider with exact include constraint that does not match it": {
			MultiSourceSelector{
				Source:  emptySource,
				Include: mustParseMultiSourceMatchingPatterns("hashicorp/foo"),
			},
			addrs.NewBuiltInProvider("bar"),
			false,
		},
		"built-in provider with type-wild include constraint that does not match it": {
			MultiSourceSelector{
				Source:  emptySource,
				Include: mustParseMultiSourceMatchingPatterns("hashicorp/*"),
			},
			addrs.NewBuiltInProvider("bar"),
			false,
		},
		"built-in provider with namespace-wild include constraint that does not match it": {
			MultiSourceSelector{
				Source:  emptySource,
				Include: mustParseMultiSourceMatchingPatterns("*/*"),
			},
			// Doesn't match because builtin providers are in "terraform.io",
			// but a pattern with no hostname is for registry.opentofu.org.
			addrs.NewBuiltInProvider("bar"),
			false,
		},
		"built-in provider with include constraint that matches it via type wildcard": {
			MultiSourceSelector{
				Source:  emptySource,
				Include: mustParseMultiSourceMatchingPatterns("terraform.io/builtin/*"),
			},
			addrs.NewBuiltInProvider("bar"),
			true,
		},

		// Exclude constraints
		"default provider with exclude constraint that matches it exactly": {
			MultiSourceSelector{
				Source:  emptySource,
				Exclude: mustParseMultiSourceMatchingPatterns("hashicorp/foo"),
			},
			addrs.NewDefaultProvider("foo"),
			false,
		},
		"default provider with exclude constraint that matches it via type wildcard": {
			MultiSourceSelector{
				Source:  emptySource,
				Exclude: mustParseMultiSourceMatchingPatterns("hashicorp/*"),
			},
			addrs.NewDefaultProvider("foo"),
			false,
		},
		"default provider with exact exclude constraint that doesn't match it": {
			MultiSourceSelector{
				Source:  emptySource,
				Exclude: mustParseMultiSourceMatchingPatterns("hashicorp/bar"),
			},
			addrs.NewDefaultProvider("foo"),
			true,
		},
		"default provider with non-normalized exclude constraint that matches it via type wildcard": {
			MultiSourceSelector{
				Source:  emptySource,
				Exclude: mustParseMultiSourceMatchingPatterns("HashiCorp/*"),
			},
			addrs.NewDefaultProvider("foo"),
			false,
		},

		// Both include and exclude in a single selector
		"default provider with exclude wildcard overriding include exact": {
			MultiSourceSelector{
				Source:  emptySource,
				Include: mustParseMultiSourceMatchingPatterns("hashicorp/foo"),
				Exclude: mustParseMultiSourceMatchingPatterns("hashicorp/*"),
			},
			addrs.NewDefaultProvider("foo"),
			false,
		},
		"default provider with exclude wildcard overriding irrelevant include exact": {
			MultiSourceSelector{
				Source:  emptySource,
				Include: mustParseMultiSourceMatchingPatterns("hashicorp/bar"),
				Exclude: mustParseMultiSourceMatchingPatterns("hashicorp/*"),
			},
			addrs.NewDefaultProvider("foo"),
			false,
		},
		"default provider with exclude exact overriding include wildcard": {
			MultiSourceSelector{
				Source:  emptySource,
				Include: mustParseMultiSourceMatchingPatterns("hashicorp/*"),
				Exclude: mustParseMultiSourceMatchingPatterns("hashicorp/foo"),
			},
			addrs.NewDefaultProvider("foo"),
			false,
		},
		"default provider with irrelevant exclude exact overriding include wildcard": {
			MultiSourceSelector{
				Source:  emptySource,
				Include: mustParseMultiSourceMatchingPatterns("hashicorp/*"),
				Exclude: mustParseMultiSourceMatchingPatterns("hashicorp/bar"),
			},
			addrs.NewDefaultProvider("foo"),
			true,
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			t.Logf("include:  %s", test.Selector.Include)
			t.Logf("exclude:  %s", test.Selector.Exclude)
			t.Logf("provider: %s", test.Provider)
			got := test.Selector.CanHandleProvider(test.Provider)
			want := test.WantMatch
			if got != want {
				t.Errorf("wrong result %t; want %t", got, want)
			}
		})
	}
}

func mustParseMultiSourceMatchingPatterns(strs ...string) MultiSourceMatchingPatterns {
	ret, err := ParseMultiSourceMatchingPatterns(strs)
	if err != nil {
		panic(err)
	}
	return ret
}
