package infra

import (
	"encoding/binary"
	"encoding/json"
	"net"
	"net/http"
	"strings"
	"testing"
	"time"
)

// newUnixDockerServer levanta un httptest-style server en un socket Unix temporal.
// Retorna el server (ya iniciado) y el path al socket.
func newUnixDockerServer(t *testing.T, handler http.Handler) (socketPath string) {
	t.Helper()

	socketPath = t.TempDir() + "/docker_exec_test.sock"
	ln, err := net.Listen("unix", socketPath)
	if err != nil {
		t.Fatalf("listen unix %s: %v", socketPath, err)
	}

	srv := &http.Server{Handler: handler}
	go srv.Serve(ln) //nolint:errcheck
	t.Cleanup(func() {
		srv.Close()
		ln.Close()
	})
	return socketPath
}

// buildDockerExecHandler construye un http.Handler que simula el flujo completo del
// Docker Engine API para exec: create → start (stream multiplexado) → inspect.
func buildDockerExecHandler(t *testing.T, exitCode int, stdoutPayload, stderrPayload string, delayStart time.Duration) http.Handler {
	t.Helper()

	const fakeExecID = "deadbeefcafe"

	// Construir stream multiplexado de Docker (Tty=false).
	// Frame: [type(1)] [0 0 0(3)] [size big-endian uint32(4)] [payload]
	buildFrame := func(streamType byte, payload string) []byte {
		if payload == "" {
			return nil
		}
		b := make([]byte, 8+len(payload))
		b[0] = streamType
		binary.BigEndian.PutUint32(b[4:8], uint32(len(payload)))
		copy(b[8:], payload)
		return b
	}

	var streamBody []byte
	streamBody = append(streamBody, buildFrame(1, stdoutPayload)...)
	streamBody = append(streamBody, buildFrame(2, stderrPayload)...)

	mux := http.NewServeMux()

	// POST /containers/{id}/exec — devuelve ExecID.
	mux.HandleFunc("/containers/", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost || !strings.HasSuffix(r.URL.Path, "/exec") {
			http.NotFound(w, r)
			return
		}
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusCreated)
		json.NewEncoder(w).Encode(map[string]string{"Id": fakeExecID}) //nolint:errcheck
	})

	// POST /exec/{id}/start — transmite stream multiplexado.
	// GET  /exec/{id}/json  — devuelve ExitCode.
	mux.HandleFunc("/exec/", func(w http.ResponseWriter, r *http.Request) {
		switch {
		case strings.HasSuffix(r.URL.Path, "/start") && r.Method == http.MethodPost:
			if delayStart > 0 {
				select {
				case <-r.Context().Done():
					http.Error(w, "cancelled", http.StatusGatewayTimeout)
					return
				case <-time.After(delayStart):
				}
			}
			w.WriteHeader(http.StatusOK)
			if len(streamBody) > 0 {
				w.Write(streamBody) //nolint:errcheck
			}

		case strings.HasSuffix(r.URL.Path, "/json") && r.Method == http.MethodGet:
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(http.StatusOK)
			json.NewEncoder(w).Encode(map[string]int{"ExitCode": exitCode}) //nolint:errcheck

		default:
			http.NotFound(w, r)
		}
	})

	return mux
}

func TestDockerContainerExec(t *testing.T) {
	const containerID = "abc123container"

	t.Run("binario en whitelist exitcode 0 stdout stderr capturados", func(t *testing.T) {
		handler := buildDockerExecHandler(t, 0, "hello stdout\n", "hello stderr\n", 0)
		socketPath := newUnixDockerServer(t, handler)

		result, err := DockerContainerExec(DockerExecOpts{
			ContainerID:     containerID,
			Cmd:             []string{"ls", "-la"},
			BinariesAllowed: []string{"ls", "cat", "echo"},
			DockerHost:      "unix://" + socketPath,
		})
		if err != nil {
			t.Fatalf("unexpected error: %v", err)
		}
		if result.ExitCode != 0 {
			t.Errorf("ExitCode: got %d, want 0", result.ExitCode)
		}
		if result.Stdout != "hello stdout\n" {
			t.Errorf("Stdout: got %q, want %q", result.Stdout, "hello stdout\n")
		}
		if result.Stderr != "hello stderr\n" {
			t.Errorf("Stderr: got %q, want %q", result.Stderr, "hello stderr\n")
		}
		if result.Duration < 0 {
			t.Errorf("Duration should be >= 0, got %d", result.Duration)
		}
	})

	t.Run("binario NO en whitelist error sin contactar engine", func(t *testing.T) {
		// No levantamos server: si se contacta el engine, falla con connection refused.
		// La validacion debe fallar ANTES de intentar conectar.
		_, err := DockerContainerExec(DockerExecOpts{
			ContainerID:     containerID,
			Cmd:             []string{"bash", "-c", "rm -rf /"},
			BinariesAllowed: []string{"ls", "cat"},
			DockerHost:      "unix:///tmp/nonexistent-docker-for-test.sock",
		})
		if err == nil {
			t.Fatal("expected error for binary not in whitelist, got nil")
		}
		if !strings.Contains(err.Error(), "not in whitelist") {
			t.Errorf("error should mention whitelist, got: %v", err)
		}
	})

	t.Run("whitelist vacia rechaza todo", func(t *testing.T) {
		_, err := DockerContainerExec(DockerExecOpts{
			ContainerID:     containerID,
			Cmd:             []string{"ls"},
			BinariesAllowed: []string{},
			DockerHost:      "unix:///tmp/nonexistent-docker-for-test.sock",
		})
		if err == nil {
			t.Fatal("expected error for empty whitelist, got nil")
		}
		if !strings.Contains(err.Error(), "no binaries whitelisted") {
			t.Errorf("error should mention empty whitelist, got: %v", err)
		}
	})

	t.Run("timeout simulado", func(t *testing.T) {
		// El handler demora 2s en /start, el timeout de la funcion es 1s.
		handler := buildDockerExecHandler(t, 0, "should not arrive", "", 2*time.Second)
		socketPath := newUnixDockerServer(t, handler)

		done := make(chan error, 1)
		go func() {
			_, err := DockerContainerExec(DockerExecOpts{
				ContainerID:     containerID,
				Cmd:             []string{"sleep"},
				BinariesAllowed: []string{"sleep"},
				DockerHost:      "unix://" + socketPath,
				TimeoutSeconds:  1,
			})
			done <- err
		}()

		select {
		case err := <-done:
			if err == nil {
				t.Fatal("expected timeout error, got nil")
			}
			// Cualquier error de contexto/red es valido (context deadline exceeded, connection reset, etc.)
		case <-time.After(5 * time.Second):
			t.Fatal("test itself timed out — DockerContainerExec did not respect TimeoutSeconds")
		}
	})
}
