package agent

import (
	"encoding/json"
	"fmt"
	"net"
	"net/http"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"sync/atomic"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
)

func writeFakeOpenCodeServeBinary(t *testing.T) string {
	t.Helper()

	exe, err := os.Executable()
	require.NoError(t, err)

	dir := t.TempDir()
	if runtime.GOOS == "windows" {
		path := filepath.Join(dir, "opencode.cmd")
		script := fmt.Sprintf("@echo off\r\nset AGENTTEAM_TEST_FAKE_OPENCODE=1\r\n\"%s\" -test.run=TestOpenCodeServeHelper -- %%*\r\n", exe)
		require.NoError(t, os.WriteFile(path, []byte(script), 0o755))
		return path
	}

	path := filepath.Join(dir, "opencode")
	script := fmt.Sprintf("#!/bin/sh\nAGENTTEAM_TEST_FAKE_OPENCODE=1 exec %s -test.run=TestOpenCodeServeHelper -- \"$@\"\n", testShellQuote(exe))
	require.NoError(t, os.WriteFile(path, []byte(script), 0o755))
	return path
}

func testShellQuote(value string) string {
	return "'" + strings.ReplaceAll(value, "'", `'\''`) + "'"
}

func TestOpenCodeServeHelper(t *testing.T) {
	if os.Getenv("AGENTTEAM_TEST_FAKE_OPENCODE") != "1" {
		return
	}
	runFakeOpenCodeServe()
	os.Exit(0)
}

func runFakeOpenCodeServe() {
	host := fakeOpenCodeArg("--hostname")
	if host == "" {
		host = "127.0.0.1"
	}
	port := fakeOpenCodeArg("--port")
	if port == "" {
		fmt.Fprintln(os.Stderr, "missing --port")
		os.Exit(2)
	}
	listener, err := net.Listen("tcp", net.JoinHostPort(host, port))
	if err != nil {
		fmt.Fprintf(os.Stderr, "listen: %v\n", err)
		os.Exit(2)
	}

	var eventReady atomic.Bool
	mux := http.NewServeMux()
	mux.HandleFunc("/global/health", func(w http.ResponseWriter, r *http.Request) {
		writeFakeOpenCodeMarker("health_requested")
		if os.Getenv("AGENTTEAM_TEST_FAKE_OPENCODE_BLOCK_HEALTH") == "1" &&
			!waitForFakeOpenCodeMarker("release_health", r.Context().Done()) {
			return
		}
		if os.Getenv("AGENTTEAM_TEST_FAKE_OPENCODE_HEALTH_FAILURE") == "1" {
			http.Error(w, "still warming", http.StatusServiceUnavailable)
			return
		}
		w.WriteHeader(http.StatusOK)
	})
	mux.HandleFunc("/session", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost {
			http.NotFound(w, r)
			return
		}
		if os.Getenv("AGENTTEAM_TEST_FAKE_OPENCODE_SESSION_FAILURE") == "1" {
			http.Error(w, "session unavailable", http.StatusServiceUnavailable)
			return
		}
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(`{"id":"sess-1"}`))
	})
	mux.HandleFunc("/session/sess-1", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodGet {
			http.NotFound(w, r)
			return
		}
		w.Header().Set("Content-Type", "application/json")
		if os.Getenv("AGENTTEAM_TEST_FAKE_OPENCODE_PENDING_REVERT") == "1" {
			_, _ = w.Write([]byte(`{"id":"sess-1","agent":"build","revert":{"messageID":"msg_100000000001AAAAAAAAAAAAAA"}}`))
			return
		}
		_, _ = w.Write([]byte(`{"id":"sess-1","agent":"build"}`))
	})
	mux.HandleFunc("/event", func(w http.ResponseWriter, r *http.Request) {
		if os.Getenv("AGENTTEAM_TEST_FAKE_OPENCODE_EXIT_ON_EVENT") == "1" {
			fmt.Fprintln(os.Stderr, "fake opencode event startup failure")
			os.Exit(42)
		}
		w.Header().Set("Content-Type", "text/event-stream")
		w.WriteHeader(http.StatusOK)
		if flusher, ok := w.(http.Flusher); ok {
			flusher.Flush()
		}
		writeFakeOpenCodeMarker("event_headers_flushed")
		if !waitForFakeOpenCodeMarker("release_event_data", r.Context().Done()) {
			return
		}
		eventReady.Store(true)
		writeFakeOpenCodeMarker("event_connected")
		if flusher, ok := w.(http.Flusher); ok {
			_, _ = w.Write([]byte(`data: {"type":"server.connected","properties":{}}` + "\n\n"))
			flusher.Flush()
		}
		<-r.Context().Done()
	})
	mux.HandleFunc("/session/sess-1/message", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodGet || strings.TrimSpace(os.Getenv("AGENTTEAM_TEST_FAKE_OPENCODE_RESUME_TAIL_ROLE")) == "" {
			http.NotFound(w, r)
			return
		}
		if !eventReady.Load() {
			writeFakeOpenCodeMarker("resume_messages_before_event")
			http.Error(w, "message tail before event stream", http.StatusConflict)
			return
		}
		writeFakeOpenCodeMarker("resume_messages_after_event")
		role := strings.TrimSpace(os.Getenv("AGENTTEAM_TEST_FAKE_OPENCODE_RESUME_TAIL_ROLE"))
		if role == "failure" {
			http.Error(w, "message tail unavailable", http.StatusServiceUnavailable)
			return
		}
		w.Header().Set("Content-Type", "application/json")
		_ = json.NewEncoder(w).Encode([]map[string]any{{
			"info": map[string]string{
				"id":   "msg_100000000001AAAAAAAAAAAAAA",
				"role": role,
			},
			"parts": []map[string]string{},
		}})
	})
	mux.HandleFunc("/session/sess-1/prompt_async", func(w http.ResponseWriter, r *http.Request) {
		if !eventReady.Load() {
			writeFakeOpenCodeMarker("prompt_before_event")
			http.Error(w, "prompt before event stream", http.StatusConflict)
			return
		}
		var body struct {
			MessageID string           `json:"messageID"`
			Parts     []map[string]any `json:"parts"`
		}
		if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
			http.Error(w, "invalid prompt body", http.StatusBadRequest)
			return
		}
		switch os.Getenv("AGENTTEAM_TEST_FAKE_OPENCODE_PROMPT_FAILURE") {
		case "status":
			http.Error(w, "prompt rejected", http.StatusBadGateway)
			return
		case "eof":
			hijacker, ok := w.(http.Hijacker)
			if !ok {
				http.Error(w, "hijacking unavailable", http.StatusInternalServerError)
				return
			}
			conn, _, err := hijacker.Hijack()
			if err == nil {
				_ = conn.Close()
			}
			return
		}
		if body.MessageID == "msg_100000000001AAAAAAAAAAAAAA" && len(body.Parts) == 0 {
			writeFakeOpenCodeMarker("resume_tail_kicked")
			if os.Getenv("AGENTTEAM_TEST_FAKE_OPENCODE_BLOCK_RESUME_PROMPT") == "1" &&
				!waitForFakeOpenCodeMarker("release_resume_prompt", r.Context().Done()) {
				return
			}
		}
		writeFakeOpenCodeMarker("prompt_after_event")
		w.WriteHeader(http.StatusNoContent)
	})

	if err := http.Serve(listener, mux); err != nil && err != http.ErrServerClosed {
		fmt.Fprintf(os.Stderr, "serve: %v\n", err)
		os.Exit(2)
	}
}

func fakeOpenCodeArg(name string) string {
	for i, arg := range os.Args {
		if arg == name && i+1 < len(os.Args) {
			return os.Args[i+1]
		}
	}
	return ""
}

func waitForFakeOpenCodeMarker(name string, done <-chan struct{}) bool {
	dir := strings.TrimSpace(os.Getenv("AGENTTEAM_TEST_FAKE_OPENCODE_RECORD_DIR"))
	if dir == "" {
		return true
	}
	path := filepath.Join(dir, name)
	ticker := time.NewTicker(10 * time.Millisecond)
	defer ticker.Stop()
	for {
		if fileExists(path) {
			return true
		}
		select {
		case <-done:
			return false
		case <-ticker.C:
		}
	}
}

func writeFakeOpenCodeMarker(name string) {
	dir := strings.TrimSpace(os.Getenv("AGENTTEAM_TEST_FAKE_OPENCODE_RECORD_DIR"))
	if dir == "" {
		return
	}
	_ = os.WriteFile(filepath.Join(dir, name), []byte(time.Now().Format(time.RFC3339Nano)), 0o644)
}

func fileExists(path string) bool {
	_, err := os.Stat(path)
	return err == nil
}
