package testenv

import (
	"context"
	"crypto/ed25519"
	"crypto/rand"
	"crypto/x509"
	"encoding/pem"
	"fmt"
	"path/filepath"
	"testing"
	"time"

	"github.com/testcontainers/testcontainers-go"
	tcnetwork "github.com/testcontainers/testcontainers-go/network"
	"github.com/testcontainers/testcontainers-go/wait"
)

// GatewayContainer wraps a running galaxy/gateway:integration
// container.
type GatewayContainer struct {
	Container testcontainers.Container
	HTTPHost  string
	HTTPPort  int
	HTTPURL   string
	GRPCHost  string
	GRPCPort  int
	GRPCAddr  string

	// ResponseSignerPublic is the Ed25519 public key the gateway uses
	// to sign responses and push events. Tests verify signatures
	// against this value.
	ResponseSignerPublic ed25519.PublicKey
}

// GatewayOptions tunes a gateway container before it boots.
type GatewayOptions struct {
	NetworkAlias    string
	NetworkName     string
	BackendHTTPURL  string
	BackendGRPCURL  string
	RedisAddr       string
	GatewayClientID string
	Extra           map[string]string
}

// StartGateway boots galaxy/gateway:integration with the supplied
// options.
func StartGateway(t *testing.T, opts GatewayOptions) *GatewayContainer {
	t.Helper()
	EnsureGatewayImage(t)

	if opts.NetworkAlias == "" {
		opts.NetworkAlias = "gateway"
	}
	if opts.GatewayClientID == "" {
		opts.GatewayClientID = "integration-gateway"
	}

	pub, priv, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		t.Fatalf("generate ed25519 key: %v", err)
	}
	keyDER, err := x509.MarshalPKCS8PrivateKey(priv)
	if err != nil {
		t.Fatalf("marshal ed25519 key: %v", err)
	}
	keyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER})
	keyPath := filepath.Join(t.TempDir(), "gateway-signer.pem")
	if err := writeFile(keyPath, keyPEM); err != nil {
		t.Fatalf("write signer key: %v", err)
	}

	containerKey := "/etc/galaxy/gateway-signer.pem"
	env := map[string]string{
		"GATEWAY_PUBLIC_HTTP_ADDR":                     ":8080",
		"GATEWAY_AUTHENTICATED_GRPC_ADDR":              ":9090",
		"GATEWAY_LOG_LEVEL":                            "debug",
		"GATEWAY_REDIS_MASTER_ADDR":                    opts.RedisAddr,
		"GATEWAY_REDIS_PASSWORD":                       RedisIntegrationPassword,
		"GATEWAY_BACKEND_HTTP_URL":                     opts.BackendHTTPURL,
		"GATEWAY_BACKEND_GRPC_PUSH_URL":                opts.BackendGRPCURL,
		"GATEWAY_BACKEND_GATEWAY_CLIENT_ID":            opts.GatewayClientID,
		"GATEWAY_RESPONSE_SIGNER_PRIVATE_KEY_PEM_PATH": containerKey,
		// Loosen anti-abuse so happy-path scenarios aren't rate-limited.
		// Negative-path edge tests tighten these per-test.
		"GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_PUBLIC_AUTH_RATE_LIMIT_REQUESTS":         "10000",
		"GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_PUBLIC_AUTH_RATE_LIMIT_BURST":            "1000",
		// Identity-bucket limits sit on top of the class limits and are
		// keyed by the request identity (email for send-email-code,
		// challenge_id for confirm-email-code). The defaults are
		// purposely tight in production (3 sends per email per window);
		// happy-path scenarios that re-issue codes for the same email
		// would otherwise trip the limiter mid-test.
		"GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_SEND_EMAIL_CODE_IDENTITY_RATE_LIMIT_REQUESTS":    "10000",
		"GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_SEND_EMAIL_CODE_IDENTITY_RATE_LIMIT_BURST":       "1000",
		"GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_CONFIRM_EMAIL_CODE_IDENTITY_RATE_LIMIT_REQUESTS": "10000",
		"GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_CONFIRM_EMAIL_CODE_IDENTITY_RATE_LIMIT_BURST":    "1000",
		"GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_IP_RATE_LIMIT_REQUESTS":           "10000",
		"GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_IP_RATE_LIMIT_BURST":              "1000",
		"GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_SESSION_RATE_LIMIT_REQUESTS":      "10000",
		"GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_SESSION_RATE_LIMIT_BURST":         "1000",
		"GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_USER_RATE_LIMIT_REQUESTS":         "10000",
		"GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_USER_RATE_LIMIT_BURST":            "1000",
		"GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_MESSAGE_CLASS_RATE_LIMIT_REQUESTS": "10000",
		"GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_MESSAGE_CLASS_RATE_LIMIT_BURST":    "1000",
	}
	for k, v := range opts.Extra {
		env[k] = v
	}

	req := testcontainers.ContainerRequest{
		Image:        GatewayImage,
		ExposedPorts: []string{"8080/tcp", "9090/tcp"},
		Env:          env,
		WaitingFor: wait.ForHTTP("/healthz").
			WithPort("8080/tcp").
			WithStartupTimeout(60 * time.Second),
		Files: []testcontainers.ContainerFile{
			{
				HostFilePath:      keyPath,
				ContainerFilePath: containerKey,
				// 0o444 so the distroless `nonroot` user (uid 65532)
				// inside the gateway image can read the integration
				// signer key. The key is ephemeral and never leaves
				// the test process, so widening the mode is safe.
				FileMode: 0o444,
			},
		},
	}

	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 gateway container: %v", err)
	}
	t.Cleanup(func() {
		if err := testcontainers.TerminateContainer(container); err != nil {
			t.Logf("terminate gateway: %v", err)
		}
	})

	host, err := container.Host(ctx)
	if err != nil {
		t.Fatalf("gateway host: %v", err)
	}
	port, err := container.MappedPort(ctx, "8080/tcp")
	if err != nil {
		t.Fatalf("gateway port: %v", err)
	}
	grpcPort, err := container.MappedPort(ctx, "9090/tcp")
	if err != nil {
		t.Fatalf("gateway grpc port: %v", err)
	}
	return &GatewayContainer{
		Container:            container,
		HTTPHost:             host,
		HTTPPort:             int(port.Num()),
		HTTPURL:              fmt.Sprintf("http://%s:%d", host, port.Num()),
		GRPCHost:             host,
		GRPCPort:             int(grpcPort.Num()),
		GRPCAddr:             fmt.Sprintf("%s:%d", host, grpcPort.Num()),
		ResponseSignerPublic: pub,
	}
}

func writeFile(path string, content []byte) error {
	return writeFileFn(path, content)
}
