package provider

import (
	"bufio"
	"context"
	"fmt"
	"io"
	"os"
	"slices"
	"strings"
	"testing"
	"text/template"

	"github.com/coder/terraform-provider-envbuilder/testutil/registrytest"
	"github.com/hashicorp/terraform-plugin-framework/providerserver"
	"github.com/hashicorp/terraform-plugin-go/tfprotov6"

	"github.com/docker/docker/api/types/container"
	"github.com/docker/docker/api/types/image"
	"github.com/docker/docker/client"
	"github.com/stretchr/testify/require"
)

const (
	testContainerLabel = "terraform-provider-envbuilder-test"
)

// testAccProtoV6ProviderFactories are used to instantiate a provider during
// acceptance testing. The factory function will be invoked for every Terraform
// CLI command executed to create a provider server to which the CLI can
// reattach.
var testAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){
	"envbuilder": providerserver.NewProtocol6WithError(New("test")()),
}

// testDependencies contain information about stuff the test depends on.
type testDependencies struct {
	BuilderImage string
	CacheRepo    string
	ExtraEnv     map[string]string
	Repo         testGitRepoSSH
}

// Config generates a valid Terraform config file from the dependencies.
func (d *testDependencies) Config(t testing.TB) string {
	t.Helper()

	tpl := `provider envbuilder {}
resource "envbuilder_cached_image" "test" {
  builder_image            = {{ quote .BuilderImage }}
	cache_repo               = {{ quote .CacheRepo }}
	extra_env                = {
	{{ range $k, $v := .ExtraEnv }}
		{{ quote $k }}: {{ quote $v }}
	{{ end }}
	}
	git_url                  = {{ quote .Repo.URL }}
	git_ssh_private_key_path = {{ quote .Repo.Key }}
	verbose                  = true
	workspace_folder         = {{ quote .Repo.Dir }}
}`

	fm := template.FuncMap{"quote": quote}
	var sb strings.Builder
	tmpl, err := template.New("envbuilder_cached_image").Funcs(fm).Parse(tpl)
	require.NoError(t, err)
	require.NoError(t, tmpl.Execute(&sb, d))
	return sb.String()
}

func quote(s string) string {
	return fmt.Sprintf("%q", s)
}

func setup(ctx context.Context, t testing.TB, files map[string]string) testDependencies {
	t.Helper()

	envbuilderImage := getEnvOrDefault("ENVBUILDER_IMAGE", "ghcr.io/coder/envbuilder-preview")
	envbuilderVersion := getEnvOrDefault("ENVBUILDER_VERSION", "latest")
	envbuilderImageRef := envbuilderImage + ":" + envbuilderVersion

	// TODO: envbuilder creates /.envbuilder/bin/envbuilder owned by root:root which we are unable to clean up.
	// This causes tests to fail.
	regDir := t.TempDir()
	reg := registrytest.New(t, regDir)

	repoDir := setupGitRepo(t, files)
	gitRepo := serveGitRepoSSH(ctx, t, repoDir)

	return testDependencies{
		BuilderImage: envbuilderImageRef,
		CacheRepo:    reg + "/test",
		ExtraEnv:     make(map[string]string),
		Repo:         gitRepo,
	}
}

func seedCache(ctx context.Context, t testing.TB, deps testDependencies) {
	t.Helper()

	t.Logf("seeding cache with %s", deps.CacheRepo)
	defer t.Logf("finished seeding cache with %s", deps.CacheRepo)

	cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
	require.NoError(t, err, "init docker client")
	t.Cleanup(func() { _ = cli.Close() })

	ensureImage(ctx, t, cli, deps.BuilderImage)

	// Run envbuilder using this dir as a local layer cache
	ctr, err := cli.ContainerCreate(ctx, &container.Config{
		Image: deps.BuilderImage,
		Env: []string{
			"ENVBUILDER_CACHE_REPO=" + deps.CacheRepo,
			"ENVBUILDER_EXIT_ON_BUILD_FAILURE=true",
			"ENVBUILDER_INIT_SCRIPT=exit",
			"ENVBUILDER_PUSH_IMAGE=true",
			"ENVBUILDER_VERBOSE=true",
			"ENVBUILDER_GIT_URL=" + deps.Repo.URL,
			"ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH=/id_ed25519",
		},
		Labels: map[string]string{
			testContainerLabel: "true",
		},
	}, &container.HostConfig{
		NetworkMode: container.NetworkMode("host"),
		Binds: []string{
			deps.Repo.Key + ":/id_ed25519",
		},
	}, nil, nil, "")

	require.NoError(t, err, "failed to run envbuilder to seed cache")
	t.Cleanup(func() {
		_ = cli.ContainerRemove(ctx, ctr.ID, container.RemoveOptions{
			RemoveVolumes: true,
			Force:         true,
		})
	})
	err = cli.ContainerStart(ctx, ctr.ID, container.StartOptions{})
	require.NoError(t, err)

	rawLogs, err := cli.ContainerLogs(ctx, ctr.ID, container.LogsOptions{
		ShowStdout: true,
		ShowStderr: true,
		Follow:     true,
		Timestamps: false,
	})
	require.NoError(t, err)
	defer rawLogs.Close()
	scanner := bufio.NewScanner(rawLogs)
SCANLOGS:
	for {
		select {
		case <-ctx.Done():
			require.Fail(t, "envbuilder did not finish running in time")
		default:
			if !scanner.Scan() {
				require.Fail(t, "envbuilder did not run successfully")
			}
			log := scanner.Text()
			t.Logf("envbuilder: %s", log)
			if strings.Contains(log, "=== Running the init command") {
				break SCANLOGS
			}
		}
	}
}

func getEnvOrDefault(env, defVal string) string {
	if val := os.Getenv(env); val != "" {
		return val
	}
	return defVal
}

func ensureImage(ctx context.Context, t testing.TB, cli *client.Client, ref string) {
	t.Helper()

	t.Logf("ensuring image %q", ref)
	images, err := cli.ImageList(ctx, image.ListOptions{})
	require.NoError(t, err, "list images")
	for _, img := range images {
		if strings.HasSuffix(ref, ":latest") {
			t.Logf("always pull latest")
			break
		} else if slices.Contains(img.RepoTags, ref) {
			t.Logf("image %q found locally, not pulling", ref)
			return
		}
	}
	t.Logf("attempting to pull image %q", ref)
	resp, err := cli.ImagePull(ctx, ref, image.PullOptions{})
	require.NoError(t, err)
	_, err = io.ReadAll(resp)
	require.NoError(t, err)
}

// quotedPrefix is a helper for asserting quoted strings.
func quotedPrefix(prefix string) func(string) error {
	return func(val string) error {
		trimmed := strings.Trim(val, `"`)
		if !strings.HasPrefix(trimmed, prefix) {
			return fmt.Errorf("expected value %q to have prefix %q", trimmed, prefix)
		}
		return nil
	}
}