package main_test

import (
	"context"
	"fmt"
	"net"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"testing"
	"time"

	"github.com/rogpeppe/go-internal/testscript"
	"gopkg.in/yaml.v2"

	"olexsmir.xyz/mugit/internal/config"
	"olexsmir.xyz/mugit/internal/handlers"
)

var (
	mugitBin   string
	httpPort   int
	reposDir   string
	configPath string
)

func TestMain(m *testing.M) { os.Exit(testMain(m)) }
func testMain(m *testing.M) int {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	tmpDir, err := os.MkdirTemp("", "mugit-test-*")
	if err != nil {
		fmt.Fprintf(os.Stderr, "failed to create temp dir: %v\n", err)
		return 1
	}
	defer func() { _ = os.RemoveAll(tmpDir) }()

	reposDir = filepath.Join(tmpDir, "repos")
	if jerr := os.MkdirAll(reposDir, 0o755); jerr != nil {
		fmt.Fprintf(os.Stderr, "failed to create repo dir: %v\n", jerr)
		return 1
	}

	if berr := buildMugitBinary(tmpDir); berr != nil {
		fmt.Fprintf(os.Stderr, "failed to build binary: %v\n", berr)
		return 1
	}

	port, err := findFreePort()
	if err != nil {
		fmt.Fprintf(os.Stderr, "failed to find free port: %v\n", err)
		return 1
	}
	httpPort = port

	cfg := &config.Config{
		Server: config.ServerConfig{
			Host: "127.0.0.1",
			Port: httpPort,
		},
		Meta: config.MetaConfig{
			Title: "test mugit",
			Host:  "localhost",
			Modt:  "Welcome to test mugit!",
		},
		Repo: config.RepoConfig{
			Dir:     reposDir,
			Readmes: []string{"README.md"},
		},
		SSH:    config.SSHConfig{Enable: true, User: "git"},
		Mirror: config.MirrorConfig{Enable: false},
		Cache: config.CacheConfig{
			HomePage: 0,
			Readme:   0,
			Diff:     0,
		},
	}

	configPath = filepath.Join(tmpDir, "config.yaml")
	configBytes, err := yaml.Marshal(cfg)
	if err != nil {
		fmt.Fprintf(os.Stderr, "failed to marshal config: %v\n", err)
		return 1
	}
	if err := os.WriteFile(configPath, configBytes, 0o600); err != nil {
		fmt.Fprintf(os.Stderr, "failed to write config: %v\n", err)
		return 1
	}

	httpServer := &http.Server{
		Addr:    net.JoinHostPort(cfg.Server.Host, strconv.Itoa(cfg.Server.Port)),
		Handler: handlers.InitRoutes(cfg),
	}
	go func() {
		if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			fmt.Fprintf(os.Stderr, "HTTP server error: %v\n", err)
		}
	}()

	if err := waitForPort(httpPort, 5*time.Second); err != nil {
		fmt.Fprintf(os.Stderr, "server did not become ready: %v\n", err)
		return 1
	}

	code := m.Run()
	_ = httpServer.Shutdown(ctx)
	return code
}

func TestScript(t *testing.T) {
	if testing.Short() {
		t.Skip("skipping integration tests")
	}

	sshWrapperContent := fmt.Sprintf(`#!/bin/sh
export SSH_ORIGINAL_COMMAND="$2"
exec %s shell -c %s`, mugitBin, configPath)

	testscript.Run(t, testscript.Params{
		Dir: "testscript",
		Cmds: map[string]func(ts *testscript.TestScript, neg bool, args []string){
			"mugit": cmdMugit,
			"git":   cmdGit,
		},
		Setup: func(env *testscript.Env) error {
			work := env.Getenv("WORK")
			sshWrapperPath := filepath.Join(work, "ssh-wrapper.sh")
			if err := os.WriteFile(sshWrapperPath, []byte(sshWrapperContent), 0o700); err != nil {
				return fmt.Errorf("failed to create ssh wrapper: %w", err)
			}

			env.Setenv("SSH_WRAPPER", sshWrapperPath)
			env.Setenv("REPOS", reposDir)
			env.Setenv("MPORT", strconv.Itoa(httpPort))
			env.Setenv("MURL", fmt.Sprintf("http://127.0.0.1:%d", httpPort))
			return nil
		},
	})
}

func buildMugitBinary(tmpDir string) error {
	mugitBin = filepath.Join(tmpDir, "mugit")
	cmd := exec.Command("go", "build", "-o", mugitBin, ".")
	cmd.Dir = "."
	if out, err := cmd.CombinedOutput(); err != nil {
		_ = os.RemoveAll(tmpDir)
		return fmt.Errorf("go build: %v\n%s", err, out)
	}
	return nil
}

func findFreePort() (int, error) {
	l, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		return 0, err
	}
	port := l.Addr().(*net.TCPAddr).Port
	_ = l.Close()
	return port, nil
}

func waitForPort(port int, timeout time.Duration) error {
	deadline := time.Now().Add(timeout)
	for time.Now().Before(deadline) {
		if conn, err := net.DialTimeout(
			"tcp",
			net.JoinHostPort("127.0.0.1", strconv.Itoa(port)),
			200*time.Millisecond,
		); err == nil {
			_ = conn.Close()
			return nil
		}
		time.Sleep(50 * time.Millisecond)
	}
	return fmt.Errorf("port %d not ready after %s", port, timeout)
}

func cmdMugit(ts *testscript.TestScript, neg bool, args []string) {
	if len(args) < 1 {
		ts.Fatalf("usage: mugit <subcommand> ...")
	}
	cmd := exec.Command(mugitBin, append([]string{"-c", configPath}, args...)...)
	cmd.Env = os.Environ()
	cmd.Stdout = ts.Stdout()
	cmd.Stderr = ts.Stderr()
	err := cmd.Run()
	if neg {
		if err == nil {
			ts.Fatalf("expected mugit to fail, it succeeded")
		}
	} else {
		if err != nil {
			ts.Fatalf("mugit: %v", err)
		}
	}
}

func cmdGit(ts *testscript.TestScript, neg bool, args []string) {
	if len(args) > 0 && args[0] == "init" {
		hasBranch := false
		for _, arg := range args {
			if arg == "-b" || arg == "--initial-branch" {
				hasBranch = true
				break
			}
		}
		if !hasBranch {
			args = append([]string{"init", "-b", "master"}, args[1:]...)
		}
	}
	args = append([]string{
		"-c", "user.email=test@test.local",
		"-c", "user.name=Test User",
	}, args...)
	cmd := exec.Command("git", args...)
	cmd.Dir = ts.Getenv("WORK")
	cmd.Stdout = ts.Stdout()
	cmd.Stderr = ts.Stderr()

	err := cmd.Run()
	if err == nil && neg {
		ts.Fatalf("expected git to fail, but it succeeded")
	}
	if err != nil && !neg {
		ts.Fatalf("git: %v", err)
	}
}
