package testenv

import (
	"context"
	"fmt"
	"path/filepath"
	"testing"
	"time"

	"github.com/google/uuid"
	"github.com/moby/moby/api/types/container"
	"github.com/moby/moby/api/types/mount"
	"github.com/testcontainers/testcontainers-go"
	tcnetwork "github.com/testcontainers/testcontainers-go/network"
	"github.com/testcontainers/testcontainers-go/wait"
)

// BackendContainer wraps a running galaxy/backend:integration
// container reachable from the host (HTTPHost, GRPCPushHost) and
// from the shared Docker network at the alias "backend".
type BackendContainer struct {
	Container testcontainers.Container
	HTTPHost  string
	HTTPPort  int
	HTTPURL   string
	GRPCHost  string
	GRPCPort  int
	GRPCURL   string

	// AdminUser/AdminPassword are the bootstrap admin credentials this
	// container started with. Tests that exercise the admin surface
	// reuse them directly.
	AdminUser     string
	AdminPassword string
}

// BackendOptions tunes a backend container before it boots.
type BackendOptions struct {
	NetworkAlias  string
	NetworkName   string
	PostgresDSN   string
	MailpitHost   string
	MailpitPort   int
	GeoIPHostPath string
	AdminEmail    string
	Extra         map[string]string
}

// StartBackend boots galaxy/backend:integration with the supplied
// options.
func StartBackend(t *testing.T, opts BackendOptions) *BackendContainer {
	t.Helper()
	EnsureBackendImage(t)

	if opts.NetworkAlias == "" {
		opts.NetworkAlias = "backend"
	}
	if opts.AdminEmail == "" {
		opts.AdminEmail = "admin@galaxy.test"
	}

	geoIPInContainer := "/var/lib/galaxy/geoip.mmdb"
	// Use a unique daemon-side path for each test so concurrent
	// runs cannot collide. Docker creates the source directory at
	// container start because BindOptions.CreateMountpoint=true.
	stateRoot := "/tmp/galaxy-state-" + uuid.NewString()

	env := map[string]string{
		"BACKEND_HTTP_LISTEN_ADDR":           ":8080",
		"BACKEND_GRPC_PUSH_LISTEN_ADDR":      ":8081",
		"BACKEND_LOGGING_LEVEL":              "info",
		"BACKEND_POSTGRES_DSN":               opts.PostgresDSN,
		"BACKEND_SMTP_HOST":                  opts.MailpitHost,
		"BACKEND_SMTP_PORT":                  fmt.Sprintf("%d", opts.MailpitPort),
		"BACKEND_SMTP_FROM":                  "galaxy-backend@galaxy.test",
		"BACKEND_SMTP_TLS_MODE":              "none",
		"BACKEND_DOCKER_NETWORK":             opts.NetworkName,
		"BACKEND_GAME_STATE_ROOT":            stateRoot,
		"BACKEND_ADMIN_BOOTSTRAP_USER":       "bootstrap",
		"BACKEND_ADMIN_BOOTSTRAP_PASSWORD":   "bootstrap-secret",
		"BACKEND_GEOIP_DB_PATH":              geoIPInContainer,
		"BACKEND_OTEL_TRACES_EXPORTER":       "none",
		"BACKEND_OTEL_METRICS_EXPORTER":      "none",
		"BACKEND_NOTIFICATION_ADMIN_EMAIL":   opts.AdminEmail,
		"BACKEND_AUTH_CHALLENGE_THROTTLE_MAX": "100",
		"BACKEND_MAIL_WORKER_INTERVAL":       "500ms",
		"BACKEND_NOTIFICATION_WORKER_INTERVAL": "500ms",
	}
	for k, v := range opts.Extra {
		env[k] = v
	}

	dockerSocket := DockerSocketPath()
	req := testcontainers.ContainerRequest{
		Image:        BackendImage,
		ExposedPorts: []string{"8080/tcp", "8081/tcp"},
		Env:          env,
		WaitingFor: wait.ForHTTP("/healthz").
			WithPort("8080/tcp").
			WithStartupTimeout(60 * time.Second),
		Files: []testcontainers.ContainerFile{
			{
				HostFilePath:      opts.GeoIPHostPath,
				ContainerFilePath: geoIPInContainer,
				FileMode:          0o644,
			},
		},
		HostConfigModifier: func(hc *container.HostConfig) {
			hc.Binds = append(hc.Binds, dockerSocket+":/var/run/docker.sock")
			// Bind a unique daemon-side directory at the same path
			// inside the backend container. CreateMountpoint=true
			// asks the daemon to create the source directory if it
			// is missing, so we do not need a second container just
			// to mkdir on the daemon host. Per-game subdirectories
			// are created by backend's runtime via os.MkdirAll
			// before each engine container start.
			hc.Mounts = append(hc.Mounts, mount.Mount{
				Type:   mount.TypeBind,
				Source: stateRoot,
				Target: stateRoot,
				BindOptions: &mount.BindOptions{
					CreateMountpoint: true,
				},
			})
		},
		// The distroless `nonroot` user (uid 65532) cannot reach the
		// Docker daemon socket that backend mounts to manage engine
		// containers. In integration tests we run as root so the
		// dockerclient.EnsureNetwork startup probe succeeds; the
		// production deployment will rely on a docker-socket-proxy
		// sidecar (see ARCHITECTURE.md §13).
		User: "0:0",
	}

	gcr := &testcontainers.GenericContainerRequest{ContainerRequest: req}
	if opts.NetworkName != "" {
		_ = tcnetwork.WithNetwork([]string{opts.NetworkAlias}, &testcontainers.DockerNetwork{Name: opts.NetworkName}).Customize(gcr)
	}
	gcr.Started = true

	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
	defer cancel()
	container, err := testcontainers.GenericContainer(ctx, *gcr)
	if err != nil {
		t.Fatalf("start backend container: %v", err)
	}
	t.Cleanup(func() {
		if err := testcontainers.TerminateContainer(container); err != nil {
			t.Logf("terminate backend: %v", err)
		}
	})

	host, err := container.Host(ctx)
	if err != nil {
		t.Fatalf("backend host: %v", err)
	}
	httpPort, err := container.MappedPort(ctx, "8080/tcp")
	if err != nil {
		t.Fatalf("backend http port: %v", err)
	}
	grpcPort, err := container.MappedPort(ctx, "8081/tcp")
	if err != nil {
		t.Fatalf("backend grpc port: %v", err)
	}

	return &BackendContainer{
		Container:     container,
		HTTPHost:      host,
		HTTPPort:      int(httpPort.Num()),
		HTTPURL:       fmt.Sprintf("http://%s:%d", host, httpPort.Num()),
		GRPCHost:      host,
		GRPCPort:      int(grpcPort.Num()),
		GRPCURL:       fmt.Sprintf("%s:%d", host, grpcPort.Num()),
		AdminUser:     env["BACKEND_ADMIN_BOOTSTRAP_USER"],
		AdminPassword: env["BACKEND_ADMIN_BOOTSTRAP_PASSWORD"],
	}
}

// _ keeps filepath imported even when only the network helper grows
// here later.
var _ = filepath.Separator
