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

package helm

import (
	"context"
	"fmt"
	"io/ioutil"
	"log"
	"net"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"sync"
	"testing"

	"github.com/hashicorp/terraform-plugin-framework/provider"
	"github.com/hashicorp/terraform-plugin-framework/providerserver"
	"github.com/hashicorp/terraform-plugin-go/tfprotov6"
	"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
	v1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
	_ "k8s.io/client-go/plugin/pkg/client/auth"
	"k8s.io/client-go/tools/clientcmd"
)

const (
	testNamespacePrefix = "terraform-acc-test"
	testResourceName    = "test"
	testChartsPath      = "./testdata/charts"
	testRepositoryDir   = "./testdata/repository"
)

var (
	accTest           bool
	testRepositoryURL string
	client            kubernetes.Interface = nil
	testMeta          *Meta
)

var providerFactory map[string]func() (tfprotov6.ProviderServer, error)

func protoV6ProviderFactories() map[string]func() (tfprotov6.ProviderServer, error) {
	if len(providerFactory) != 0 {
		return providerFactory
	}

	providerFactory = map[string]func() (tfprotov6.ProviderServer, error){
		"helm": providerserver.NewProtocol6WithError(New("test")()),
	}

	return providerFactory
}

func TestMain(m *testing.M) {
	home, err := ioutil.TempDir(os.TempDir(), "helm")
	if err != nil {
		panic(err)
	}

	defer os.RemoveAll(home)

	err = os.Setenv("HELM_REPOSITORY_CONFIG", filepath.Join(home, "config/repositories.yaml"))
	if err != nil {
		panic(err)
	}

	err = os.Setenv("HELM_REPOSITORY_CACHE", filepath.Join(home, "cache/helm/repository"))
	if err != nil {
		panic(err)
	}

	err = os.Setenv("HELM_REGISTRY_CONFIG", filepath.Join(home, "config/registry.json"))
	if err != nil {
		panic(err)
	}

	err = os.Setenv("HELM_PLUGINS", filepath.Join(home, "plugins"))
	if err != nil {
		panic(err)
	}

	err = os.Setenv("XDG_CACHE_HOME", filepath.Join(home, "cache"))
	if err != nil {
		panic(err)
	}

	accTest = os.Getenv("TF_ACC") == "1"

	var stopRepositoryServer func()
	if accTest {
		_, err := exec.LookPath("helm")
		if err != nil {
			panic(`command "helm" needs to be available to run the test suite`)
		}

		// create the Kubernetes client
		c, err := createKubernetesClient()
		if err != nil {
			panic(err)
		}
		client = c

		// Build the test repository and start the server
		buildChartRepository()
		testRepositoryURL, stopRepositoryServer = startRepositoryServer()
	}

	ec := m.Run()

	if accTest {
		stopRepositoryServer()
		cleanupChartRepository()
	}

	os.Exit(ec)
}

// todo
func TestProvider(t *testing.T) {
	ctx := context.Background()
	provider := New("test")()

	// Create the provider server
	providerServer, err := createProviderServer(provider)
	if err != nil {
		t.Fatalf("Failed to create provider server: %s", err)
	}
	// Perform config validation

	validateResponse, err := providerServer.ValidateProviderConfig(ctx, &tfprotov6.ValidateProviderConfigRequest{})
	if err != nil {
		t.Fatalf("Provider config validation failed, error: %v", err)
	}

	if hasError(validateResponse.Diagnostics) {
		t.Fatalf("Provider config validation failed, diagnostics: %v", validateResponse.Diagnostics)
	}
}

func createProviderServer(provider provider.Provider) (tfprotov6.ProviderServer, error) {
	providerServerFunc := providerserver.NewProtocol6WithError(provider)
	server, err := providerServerFunc()
	if err != nil {
	} else {
	}
	return server, err
}

func buildChartRepository() {
	if _, err := os.Stat(testRepositoryDir); os.IsNotExist(err) {
		os.Mkdir(testRepositoryDir, os.ModePerm)
	}

	charts, err := ioutil.ReadDir(testChartsPath)
	if err != nil {
		panic(err)
	}

	// package all the charts
	for _, c := range charts {
		cmd := exec.Command("helm", "package", "-u",
			filepath.Join(testChartsPath, c.Name()),
			"-d", testRepositoryDir)
		out, err := cmd.CombinedOutput()
		if err != nil {
			log.Println(string(out))
			panic(err)
		}

		log.Printf("Created repository package for %q\n", c.Name())
	}

	// build the repository index
	cmd := exec.Command("helm", "repo", "index", testRepositoryDir)
	out, err := cmd.CombinedOutput()
	if err != nil {
		log.Println(string(out))
		panic(err)
	}

	log.Println("Built chart repository index")
}

func cleanupChartRepository() {
	if _, err := os.Stat(testRepositoryDir); err == nil {
		err := os.RemoveAll(testRepositoryDir)
		if err != nil {
			fmt.Println(err)
		}
	}
}

func startRepositoryServer() (string, func()) {
	wg := sync.WaitGroup{}
	wg.Add(1)

	var shutdownFunc func()
	go func() {
		fileserver := http.Server{
			Handler: http.FileServer(http.Dir(testRepositoryDir)),
		}
		fileserver.SetKeepAlivesEnabled(false)
		shutdownFunc = func() { fileserver.Shutdown(context.Background()) }
		listener, err := net.Listen("tcp", ":0")
		if err != nil {
			panic(err)
		}
		port := listener.Addr().(*net.TCPAddr).Port
		testRepositoryURL = fmt.Sprintf("http://localhost:%d", port)
		wg.Done()
		err = fileserver.Serve(listener)
		if err != nil && err != http.ErrServerClosed {
			panic(err)
		}
	}()
	wg.Wait()

	return testRepositoryURL, shutdownFunc
}

func createAndConfigureProviderServer(provider provider.Provider, ctx context.Context) (tfprotov6.ProviderServer, error) {
	log.Println("Starting createAndConfigureProviderServer...")

	providerServerFunc := providerserver.NewProtocol6WithError(provider)
	providerServer, err := providerServerFunc()
	if err != nil {
		return nil, fmt.Errorf("Failed to create protocol6 provider: %w", err)
	}
	log.Println("Provider server function created successfully.")

	configResponse, err := providerServer.ConfigureProvider(ctx, nil)
	if err != nil {
		return nil, fmt.Errorf("Error configuring provider: %w", err)
	}
	log.Println("Provider configured successfully.")

	if hasError(configResponse.Diagnostics) {
		return nil, fmt.Errorf("Provider configuration failed, diagnostics: %#v", configResponse.Diagnostics[0])
	}

	if helmProvider, ok := provider.(*HelmProvider); ok {
		testMeta = helmProvider.meta
		if testMeta == nil {
			log.Println("testMeta is nil after type assertion.")
		} else {
			log.Printf("testMeta initialized: %+v", testMeta)
		}
	} else {
		return nil, fmt.Errorf("Failed to type assert provider to HelmProvider")
	}

	return providerServer, nil
}

func testAccPreCheck(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping acceptance tests in short mode")
	}
	http.DefaultClient.CloseIdleConnections()

	ctx := context.TODO()

	provider := New("test")()

	// Create and configure the ProviderServer
	_, err := createAndConfigureProviderServer(provider, ctx)
	if err != nil {
		t.Fatalf("Pre-check failed: %v", err)
	}
}

func createKubernetesClient() (kubernetes.Interface, error) {
	rules := clientcmd.NewDefaultClientConfigLoadingRules()

	kubeconfig := os.Getenv("KUBE_CONFIG_PATH")
	if kubeconfig == "" {
		panic("Need to set KUBE_CONFIG_PATH")
	}
	rules.ExplicitPath = kubeconfig

	config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, &clientcmd.ConfigOverrides{}).ClientConfig()
	if err != nil {
		return nil, err
	}

	c, err := kubernetes.NewForConfig(config)
	if err != nil {
		return nil, err
	}
	return c, nil
}

func createRandomNamespace(t *testing.T) string {
	if !accTest {
		t.Skip("TF_ACC=1 not set")
		return ""
	}

	namespace := fmt.Sprintf("%s-%s", testNamespacePrefix, acctest.RandString(10))
	ns := &v1.Namespace{
		ObjectMeta: metav1.ObjectMeta{
			Name: namespace,
		},
	}
	_, err := client.CoreV1().Namespaces().Create(context.TODO(), ns, metav1.CreateOptions{})
	if err != nil {
		t.Fatalf("Could not create test namespace %q: %s", namespace, err)
	}
	return namespace
}

func deleteNamespace(t *testing.T, namespace string) {
	if !accTest {
		t.Skip("TF_ACC=1 not set")
		return
	}

	gracePeriodSeconds := int64(0)
	deleteOptions := metav1.DeleteOptions{
		GracePeriodSeconds: &gracePeriodSeconds,
	}
	err := client.CoreV1().Namespaces().Delete(context.TODO(), namespace, deleteOptions)
	if err != nil {
		t.Fatalf("An error occurred while deleting namespace %q: %q", namespace, err)
	}
}

func randName(prefix string) string {
	return fmt.Sprintf("%s-%s", prefix, acctest.RandString(10))
}

func hasError(diagnostics []*tfprotov6.Diagnostic) bool {
	for _, diagnostic := range diagnostics {
		if diagnostic.Severity == tfprotov6.DiagnosticSeverityError {
			return true
		}
	}
	return false
}

func DynamicValueEmpty() *tfprotov6.DynamicValue {
	return &tfprotov6.DynamicValue{
		MsgPack: nil,
		JSON:    nil,
	}
}
