// 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 (
	"maps"
	"testing"

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

func TestParseHash(t *testing.T) {
	tests := []struct {
		Input   string
		Want    Hash
		WantErr string
	}{
		{
			Input: "h1:foo",
			Want:  HashScheme1.New("foo"),
		},
		{
			Input: "zh:bar",
			Want:  HashSchemeZip.New("bar"),
		},
		{
			// A scheme we don't know is considered valid syntax, it just won't match anything.
			Input: "unknown:baz",
			Want:  HashScheme("unknown:").New("baz"),
		},
		{
			// A scheme with an empty value is weird, but allowed.
			Input: "unknown:",
			Want:  HashScheme("unknown:").New(""),
		},
		{
			Input:   "",
			WantErr: "hash string must start with a scheme keyword followed by a colon",
		},
		{
			// A naked SHA256 hash in hex format is not sufficient
			Input:   "1e5f7a5f3ade7b8b1d1d59c5cea2e1a2f8d2f8c3f41962dbbe8647e222be8239",
			WantErr: "hash string must start with a scheme keyword followed by a colon",
		},
		{
			// An empty scheme is not allowed
			Input:   ":blah",
			WantErr: "hash string must start with a scheme keyword followed by a colon",
		},
	}

	for _, test := range tests {
		t.Run(test.Input, func(t *testing.T) {
			got, err := ParseHash(test.Input)

			if test.WantErr != "" {
				if err == nil {
					t.Fatalf("want error: %s", test.WantErr)
				}
				if got, want := err.Error(), test.WantErr; got != want {
					t.Fatalf("wrong error\ngot:  %s\nwant: %s", got, want)
				}
				return
			}

			if err != nil {
				t.Fatalf("unexpected error: %s", err.Error())
			}

			if got != test.Want {
				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.Want)
			}
		})
	}
}

func TestMergeHashDisposition(t *testing.T) {
	tests := map[string]struct {
		a, b *HashDisposition
		want *HashDisposition
	}{
		"empties": {
			a:    &HashDisposition{},
			b:    &HashDisposition{},
			want: &HashDisposition{},
		},
		"empty with VerifiedLocally": {
			a: &HashDisposition{
				VerifiedLocally: true,
			},
			b: &HashDisposition{},
			want: &HashDisposition{
				VerifiedLocally: true,
			},
		},
		"empty with ReportedByRegistry": {
			a: &HashDisposition{
				ReportedByRegistry: true,
			},
			b: &HashDisposition{},
			want: &HashDisposition{
				ReportedByRegistry: true,
			},
		},
		"empty with one GPG key": {
			a: &HashDisposition{
				SignedByGPGKeyIDs: collections.NewSet("abc123"),
			},
			b: &HashDisposition{},
			want: &HashDisposition{
				SignedByGPGKeyIDs: collections.NewSet("abc123"),
			},
		},
		"many GPG keys": {
			a: &HashDisposition{
				SignedByGPGKeyIDs: collections.NewSet("abc123", "def456"),
			},
			b: &HashDisposition{
				SignedByGPGKeyIDs: collections.NewSet("def456", "ghi789"),
			},
			want: &HashDisposition{
				SignedByGPGKeyIDs: collections.NewSet("abc123", "def456", "ghi789"),
			},
		},
		"VerifiedLocally with ReportedByRegistry": {
			a: &HashDisposition{
				ReportedByRegistry: true,
			},
			b: &HashDisposition{
				VerifiedLocally: true,
			},
			want: &HashDisposition{
				ReportedByRegistry: true,
				VerifiedLocally:    true,
			},
		},
		"VerifiedLocally with itself": {
			a: &HashDisposition{
				VerifiedLocally: true,
			},
			b: &HashDisposition{
				VerifiedLocally: true,
			},
			want: &HashDisposition{
				VerifiedLocally: true,
			},
		},
		"ReportedByRegistry with itself": {
			a: &HashDisposition{
				ReportedByRegistry: true,
			},
			b: &HashDisposition{
				ReportedByRegistry: true,
			},
			want: &HashDisposition{
				ReportedByRegistry: true,
			},
		},
		"Everything at once": {
			a: &HashDisposition{
				SignedByGPGKeyIDs:  collections.NewSet("def456", "ghi789"),
				ReportedByRegistry: true,
			},
			b: &HashDisposition{
				VerifiedLocally: true,
			},
			want: &HashDisposition{
				SignedByGPGKeyIDs:  collections.NewSet("def456", "ghi789"),
				ReportedByRegistry: true,
				VerifiedLocally:    true,
			},
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			// MergeHashDisposition is supposed to be commutative, so
			// we'll test each case in both orders and expect an
			// equivalent result in each case.
			t.Run("a,b", func(t *testing.T) {
				got := MergeHashDisposition(test.a, test.b)
				if diff := cmp.Diff(test.want, got); diff != "" {
					t.Error("wrong result\n" + diff)
				}
			})
			t.Run("b,a", func(t *testing.T) {
				got := MergeHashDisposition(test.b, test.a)
				if diff := cmp.Diff(test.want, got); diff != "" {
					t.Error("wrong result\n" + diff)
				}
			})
		})
	}
}

func TestHashDispositionsMerge(t *testing.T) {
	// HashDispositions.Merge delegates to MergeHashDisposition when both
	// arguments refer to the same hash. We already have lots of tests for
	// MergeHashDisposition in TestMergeHashDisposition, so this test
	// intentionally does not duplicate all of those cases and focuses
	// only on the different cases that HashDispositions.Merge is directly
	// concernd with.

	tests := map[string]struct {
		a, b HashDispositions
		want HashDispositions
	}{
		"empties": {
			a:    HashDispositions{},
			b:    HashDispositions{},
			want: HashDispositions{},
		},
		"one into empty": {
			a: HashDispositions{},
			b: HashDispositions{
				Hash("test:foo"): &HashDisposition{
					ReportedByRegistry: true,
				},
			},
			want: HashDispositions{
				Hash("test:foo"): &HashDisposition{
					ReportedByRegistry: true,
				},
			},
		},
		"independent hashes": {
			a: HashDispositions{
				Hash("test:foo"): &HashDisposition{
					ReportedByRegistry: true,
				},
			},
			b: HashDispositions{
				Hash("test:bar"): &HashDisposition{
					VerifiedLocally: true,
				},
			},
			want: HashDispositions{
				Hash("test:foo"): &HashDisposition{
					ReportedByRegistry: true,
				},
				Hash("test:bar"): &HashDisposition{
					VerifiedLocally: true,
				},
			},
		},
		"overlapping hashes": {
			a: HashDispositions{
				Hash("test:foo"): &HashDisposition{
					ReportedByRegistry: true,
				},
			},
			b: HashDispositions{
				Hash("test:foo"): &HashDisposition{
					VerifiedLocally: true,
				},
			},
			want: HashDispositions{
				// This should be the result of MergeHashDispositions
				// on the two different entries for test:foo.
				Hash("test:foo"): &HashDisposition{
					ReportedByRegistry: true,
					VerifiedLocally:    true,
				},
			},
		},
		"mix of overlapping and independent": {
			a: HashDispositions{
				Hash("test:foo"): &HashDisposition{
					SignedByGPGKeyIDs: collections.NewSet("abc123"),
				},
				Hash("test:bar"): &HashDisposition{
					ReportedByRegistry: true,
				},
			},
			b: HashDispositions{
				Hash("test:foo"): &HashDisposition{
					SignedByGPGKeyIDs: collections.NewSet("def456"),
				},
				Hash("test:baz"): &HashDisposition{
					VerifiedLocally: true,
				},
			},
			want: HashDispositions{
				Hash("test:foo"): &HashDisposition{
					SignedByGPGKeyIDs: collections.NewSet("abc123", "def456"),
				},
				Hash("test:bar"): &HashDisposition{
					ReportedByRegistry: true,
				},
				Hash("test:baz"): &HashDisposition{
					VerifiedLocally: true,
				},
			},
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			// HashDispositions.Merge is supposed to be commutative,
			// so we'll test each case in both orders and expect an
			// equivalent result in each case.
			t.Run("a,b", func(t *testing.T) {
				// We'll make a shallow copy of test.a so that we
				// aren't directly modifying the test table, since
				// otherwise we'll pollute the input to the
				// opposite order test below.
				got := maps.Clone(test.a)
				got.Merge(test.b)
				if diff := cmp.Diff(test.want, got); diff != "" {
					t.Error("wrong result\n" + diff)
				}
			})
			t.Run("b,a", func(t *testing.T) {
				got := maps.Clone(test.b)
				got.Merge(test.a)
				if diff := cmp.Diff(test.want, got); diff != "" {
					t.Error("wrong result\n" + diff)
				}
			})
		})
	}
}

func TestEmptyPackageHashV1(t *testing.T) {
	emptyDir := t.TempDir()
	realHash, err := PackageHashV1(PackageLocalDir(emptyDir))
	if err != nil {
		t.Fatalf("failed to calculate hash of empty package: %s", err)
	}
	if realHash != emptyPackageHashV1 {
		t.Errorf("emptyPackageHashV1 does not match freshly-calculated hash of empty package\nemptyPackageHashV1: %s\ncalculated result:  %s", emptyPackageHashV1, realHash)
	}
}
