// 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"
	"fmt"
	"net/http"
	"net/http/httptest"
	"net/url"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/hashicorp/go-retryablehttp"
	svchost "github.com/hashicorp/terraform-svchost"
	svcauth "github.com/hashicorp/terraform-svchost/auth"

	"github.com/opentofu/opentofu/internal/addrs"
)

func TestHTTPMirrorSource(t *testing.T) {
	// For mirrors we require a HTTPS server, so we'll use httptest to create
	// one. However, that means we need to instantiate the source in an unusual
	// way to force it to use the test client that is configured to trust the
	// test server.
	httpServer := httptest.NewTLSServer(http.HandlerFunc(testHTTPMirrorSourceHandler))
	defer httpServer.Close()
	httpClient := httpServer.Client()
	baseURL, err := url.Parse(httpServer.URL)
	if err != nil {
		t.Fatalf("httptest.NewTLSServer returned a server with an invalid URL")
	}
	creds := svcauth.StaticCredentialsSource(map[svchost.Hostname]map[string]interface{}{
		svchost.Hostname(baseURL.Host): {
			"token": "placeholder-token",
		},
	})
	retryHTTPClient := retryablehttp.NewClient()
	retryHTTPClient.HTTPClient = httpClient
	source := newHTTPMirrorSourceWithHTTPClient(baseURL, creds, retryHTTPClient)

	existingProvider := addrs.MustParseProviderSourceString("terraform.io/test/exists")
	missingProvider := addrs.MustParseProviderSourceString("terraform.io/test/missing")
	failingProvider := addrs.MustParseProviderSourceString("terraform.io/test/fails")
	redirectingProvider := addrs.MustParseProviderSourceString("terraform.io/test/redirects")
	redirectLoopProvider := addrs.MustParseProviderSourceString("terraform.io/test/redirect-loop")
	tosPlatform := Platform{OS: "tos", Arch: "m68k"}

	t.Run("AvailableVersions for provider that exists", func(t *testing.T) {
		got, _, err := source.AvailableVersions(context.Background(), existingProvider)
		if err != nil {
			t.Fatalf("unexpected error: %s", err)
		}
		want := VersionList{
			MustParseVersion("1.0.0"),
			MustParseVersion("1.0.1"),
			MustParseVersion("1.0.2-beta.1"),
		}
		if diff := cmp.Diff(want, got); diff != "" {
			t.Errorf("wrong result\n%s", diff)
		}
	})
	t.Run("AvailableVersions for provider that doesn't exist", func(t *testing.T) {
		_, _, err := source.AvailableVersions(context.Background(), missingProvider)
		switch err := err.(type) {
		case ErrProviderNotFound:
			if got, want := err.Provider, missingProvider; got != want {
				t.Errorf("wrong provider in error\ngot:  %s\nwant: %s", got, want)
			}
		default:
			t.Fatalf("wrong error type %T; want ErrProviderNotFound", err)
		}
	})
	t.Run("AvailableVersions without required credentials", func(t *testing.T) {
		unauthSource := newHTTPMirrorSourceWithHTTPClient(baseURL, nil, retryHTTPClient)
		_, _, err := unauthSource.AvailableVersions(context.Background(), existingProvider)
		switch err := err.(type) {
		case ErrUnauthorized:
			if got, want := string(err.Hostname), baseURL.Host; got != want {
				t.Errorf("wrong hostname in error\ngot:  %s\nwant: %s", got, want)
			}
		default:
			t.Fatalf("wrong error type %T; want ErrUnauthorized", err)
		}
	})
	t.Run("AvailableVersions when the response is a server error", func(t *testing.T) {
		_, _, err := source.AvailableVersions(context.Background(), failingProvider)
		switch err := err.(type) {
		case ErrQueryFailed:
			if got, want := err.Provider, failingProvider; got != want {
				t.Errorf("wrong provider in error\ngot:  %s\nwant: %s", got, want)
			}
			if err.MirrorURL != source.baseURL {
				t.Errorf("error does not refer to the mirror URL")
			}
		default:
			t.Fatalf("wrong error type %T; want ErrQueryFailed", err)
		}
	})
	t.Run("AvailableVersions for provider that redirects", func(t *testing.T) {
		got, _, err := source.AvailableVersions(context.Background(), redirectingProvider)
		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)
		}
	})
	t.Run("AvailableVersions for provider that redirects too much", func(t *testing.T) {
		_, _, err := source.AvailableVersions(context.Background(), redirectLoopProvider)
		if err == nil {
			t.Fatalf("succeeded; expected error")
		}
	})
	t.Run("PackageMeta for a version that exists and has a hash", func(t *testing.T) {
		version := MustParseVersion("1.0.0")
		got, err := source.PackageMeta(context.Background(), existingProvider, version, tosPlatform)
		if err != nil {
			t.Fatalf("unexpected error: %s", err)
		}

		want := PackageMeta{
			Provider:       existingProvider,
			Version:        version,
			TargetPlatform: tosPlatform,
			Filename:       "terraform-provider-test_v1.0.0_tos_m68k.zip",
			Location:       PackageHTTPURL(httpServer.URL + "/terraform.io/test/exists/terraform-provider-test_v1.0.0_tos_m68k.zip"),
			Authentication: packageHashAuthentication{
				RequiredHashes: []Hash{"h1:placeholder-hash"},
				AllHashes:      []Hash{"h1:placeholder-hash", "h0:unacceptable-hash"},
				Platform:       Platform{"tos", "m68k"},
			},
		}
		if diff := cmp.Diff(want, got); diff != "" {
			t.Errorf("wrong result\n%s", diff)
		}
	})
	t.Run("PackageMeta for a version that exists and has no hash", func(t *testing.T) {
		version := MustParseVersion("1.0.1")
		got, err := source.PackageMeta(context.Background(), existingProvider, version, tosPlatform)
		if err != nil {
			t.Fatalf("unexpected error: %s", err)
		}

		want := PackageMeta{
			Provider:       existingProvider,
			Version:        version,
			TargetPlatform: tosPlatform,
			Filename:       "terraform-provider-test_v1.0.1_tos_m68k.zip",
			Location:       PackageHTTPURL(httpServer.URL + "/terraform.io/test/exists/terraform-provider-test_v1.0.1_tos_m68k.zip"),
			Authentication: nil,
		}
		if diff := cmp.Diff(want, got); diff != "" {
			t.Errorf("wrong result\n%s", diff)
		}
	})
	t.Run("PackageMeta for a version that exists but has no archives", func(t *testing.T) {
		version := MustParseVersion("1.0.2-beta.1")
		_, err := source.PackageMeta(context.Background(), existingProvider, version, tosPlatform)
		switch err := err.(type) {
		case ErrPlatformNotSupported:
			if got, want := err.Provider, existingProvider; got != want {
				t.Errorf("wrong provider in error\ngot:  %s\nwant: %s", got, want)
			}
			if got, want := err.Platform, tosPlatform; got != want {
				t.Errorf("wrong platform in error\ngot:  %s\nwant: %s", got, want)
			}
			if err.MirrorURL != source.baseURL {
				t.Errorf("error does not contain the mirror URL")
			}
		default:
			t.Fatalf("wrong error type %T; want ErrPlatformNotSupported", err)
		}
	})
	t.Run("PackageMeta with redirect to a version that exists", func(t *testing.T) {
		version := MustParseVersion("1.0.0")
		got, err := source.PackageMeta(context.Background(), redirectingProvider, version, tosPlatform)
		if err != nil {
			t.Fatalf("unexpected error: %s", err)
		}

		want := PackageMeta{
			Provider:       redirectingProvider,
			Version:        version,
			TargetPlatform: tosPlatform,
			Filename:       "terraform-provider-test.zip",

			// NOTE: The final URL is interpreted relative to the redirect
			// target, not relative to what we originally requested.
			Location: PackageHTTPURL(httpServer.URL + "/redirect-target/terraform-provider-test.zip"),
		}
		if diff := cmp.Diff(want, got); diff != "" {
			t.Errorf("wrong result\n%s", diff)
		}
	})
	t.Run("PackageMeta when the response is a server error", func(t *testing.T) {
		version := MustParseVersion("1.0.0")
		_, err := source.PackageMeta(context.Background(), failingProvider, version, tosPlatform)
		switch err := err.(type) {
		case ErrQueryFailed:
			if got, want := err.Provider, failingProvider; got != want {
				t.Errorf("wrong provider in error\ngot:  %s\nwant: %s", got, want)
			}
			if err.MirrorURL != source.baseURL {
				t.Errorf("error does not contain the mirror URL")
			}
		default:
			t.Fatalf("wrong error type %T; want ErrQueryFailed", err)
		}
	})
}

func testHTTPMirrorSourceHandler(resp http.ResponseWriter, req *http.Request) {
	if auth := req.Header.Get("authorization"); auth != "Bearer placeholder-token" {
		resp.WriteHeader(401)
		fmt.Fprintln(resp, "incorrect auth token")
	}

	switch req.URL.Path {
	case "/terraform.io/test/exists/index.json":
		resp.Header().Add("Content-Type", "application/json; ignored=yes")
		resp.WriteHeader(200)
		fmt.Fprint(resp, `
			{
				"versions": {
					"1.0.0": {},
					"1.0.1": {},
					"1.0.2-beta.1": {}
				}
			}
		`)

	case "/terraform.io/test/fails/index.json", "/terraform.io/test/fails/1.0.0.json":
		resp.WriteHeader(500)
		fmt.Fprint(resp, "server error")

	case "/terraform.io/test/exists/1.0.0.json":
		resp.Header().Add("Content-Type", "application/json; ignored=yes")
		resp.WriteHeader(200)
		fmt.Fprint(resp, `
			{
				"archives": {
					"tos_m68k": {
						"url": "terraform-provider-test_v1.0.0_tos_m68k.zip",
						"hashes": [
							"h1:placeholder-hash",
							"h0:unacceptable-hash"
						]
					}
				}
			}
		`)

	case "/terraform.io/test/exists/1.0.1.json":
		resp.Header().Add("Content-Type", "application/json; ignored=yes")
		resp.WriteHeader(200)
		fmt.Fprint(resp, `
			{
				"archives": {
					"tos_m68k": {
						"url": "terraform-provider-test_v1.0.1_tos_m68k.zip"
					}
				}
			}
		`)

	case "/terraform.io/test/exists/1.0.2-beta.1.json":
		resp.Header().Add("Content-Type", "application/json; ignored=yes")
		resp.WriteHeader(200)
		fmt.Fprint(resp, `
			{
				"archives": {}
			}
		`)

	case "/terraform.io/test/redirects/index.json":
		resp.Header().Add("location", "/redirect-target/index.json")
		resp.WriteHeader(301)
		fmt.Fprint(resp, "redirect")

	case "/redirect-target/index.json":
		resp.Header().Add("Content-Type", "application/json")
		resp.WriteHeader(200)
		fmt.Fprint(resp, `
			{
				"versions": {
					"1.0.0": {}
				}
			}
		`)

	case "/terraform.io/test/redirects/1.0.0.json":
		resp.Header().Add("location", "/redirect-target/1.0.0.json")
		resp.WriteHeader(301)
		fmt.Fprint(resp, "redirect")

	case "/redirect-target/1.0.0.json":
		resp.Header().Add("Content-Type", "application/json")
		resp.WriteHeader(200)
		fmt.Fprint(resp, `
			{
				"archives": {
					"tos_m68k": {
						"url": "terraform-provider-test.zip"
					}
				}
			}
		`)

	case "/terraform.io/test/redirect-loop/index.json":
		// This is intentionally redirecting to itself, to create a loop.
		resp.Header().Add("location", req.URL.Path)
		resp.WriteHeader(301)
		fmt.Fprint(resp, "redirect loop")

	default:
		resp.WriteHeader(404)
		fmt.Fprintln(resp, "not found")
	}
}
