package testenv

import (
	"context"
	"fmt"
	"net/url"
	"strconv"
	"testing"
	"time"

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

const (
	pgImage    = "postgres:16-alpine"
	pgUser     = "galaxy"
	pgPassword = "galaxy"
	pgDatabase = "galaxy_backend"
	pgSchema   = "backend"
	pgStartup  = 90 * time.Second
)

// Postgres holds a running Postgres testcontainer reachable from both
// the host (DSN with localhost-mapped port) and from another container
// on the same Docker network (HostInNetworkDSN).
type Postgres struct {
	container *tcpostgres.PostgresContainer
	HostDSN   string
	NetworkDSN string
}

// StartPostgres boots a postgres:16-alpine container, returns DSNs for
// both host and in-network access, and registers a t.Cleanup to
// terminate the container.
func StartPostgres(t *testing.T, network string) *Postgres {
	t.Helper()
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
	defer cancel()

	opts := []testcontainers.ContainerCustomizer{
		tcpostgres.WithDatabase(pgDatabase),
		tcpostgres.WithUsername(pgUser),
		tcpostgres.WithPassword(pgPassword),
		testcontainers.WithWaitStrategy(
			wait.ForLog("database system is ready to accept connections").
				WithOccurrence(2).
				WithStartupTimeout(pgStartup),
		),
	}
	if network != "" {
		opts = append(opts, tcnetwork.WithNetwork([]string{"postgres"}, &testcontainers.DockerNetwork{Name: network}))
	}

	container, err := tcpostgres.Run(ctx, pgImage, opts...)
	if err != nil {
		t.Skipf("postgres testcontainer unavailable: %v", err)
	}
	t.Cleanup(func() {
		if err := testcontainers.TerminateContainer(container); err != nil {
			t.Logf("terminate postgres: %v", err)
		}
	})

	hostDSN, err := container.ConnectionString(ctx, "sslmode=disable")
	if err != nil {
		t.Fatalf("postgres host DSN: %v", err)
	}
	hostDSN, err = withSearchPath(hostDSN, pgSchema)
	if err != nil {
		t.Fatalf("postgres host DSN search_path: %v", err)
	}

	networkDSN := ""
	if network != "" {
		networkDSN = buildInNetworkDSN("postgres", 5432, pgUser, pgPassword, pgDatabase, pgSchema)
	}

	return &Postgres{
		container:  container,
		HostDSN:    hostDSN,
		NetworkDSN: networkDSN,
	}
}

func withSearchPath(dsn, schema string) (string, error) {
	parsed, err := url.Parse(dsn)
	if err != nil {
		return "", err
	}
	q := parsed.Query()
	q.Set("search_path", schema)
	if q.Get("sslmode") == "" {
		q.Set("sslmode", "disable")
	}
	parsed.RawQuery = q.Encode()
	return parsed.String(), nil
}

func buildInNetworkDSN(host string, port int, user, password, db, schema string) string {
	u := &url.URL{
		Scheme:   "postgres",
		User:     url.UserPassword(user, password),
		Host:     fmt.Sprintf("%s:%d", host, port),
		Path:     "/" + db,
		RawQuery: "sslmode=disable&search_path=" + schema,
	}
	return u.String()
}

// HostPort renders a host:port pair so other testenv files can reuse
// the same formatting.
func HostPort(host string, port int) string {
	return fmt.Sprintf("%s:%d", host, port)
}

// FormatPort returns the decimal representation of port.
func FormatPort(port int) string {
	return strconv.Itoa(port)
}
