//go:build e2e

// Package e2e contains end-to-end tests for agentteam's AgentTeam channel integration.
// These tests start a real AgentTeam channel against a local fake WebSocket
// server, exercising the full bidirectional protocol.
//
// Run with: go test -tags e2e -v ./e2e/...
package e2e

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"sync"
	"testing"
	"time"

	"github.com/gorilla/websocket"
)

// ---------------------------------------------------------------------------
// Fake WebSocket server
// ---------------------------------------------------------------------------

var upgrader = websocket.Upgrader{
	CheckOrigin: func(_ *http.Request) bool { return true },
}

// fakeServer is a minimal AgentTeam-compatible WebSocket server for testing.
// It captures messages sent by the daemon (channel) and allows the test to
// inject messages toward the daemon.
type fakeServer struct {
	t   *testing.T
	srv *httptest.Server

	mu   sync.Mutex
	conn *websocket.Conn

	// fromDaemon receives messages sent by the channel to the server.
	fromDaemon chan json.RawMessage
	// toDaemon is used by tests to send messages to the channel.
	toDaemon chan []byte
}

const fakeWorkspaceID = "wsp_e2e"

// newFakeServer starts an httptest.Server that speaks the AgentTeam WebSocket
// protocol at the workspace-scoped daemon connect path. Call t.Cleanup or
// Close() to shut it down.
func newFakeServer(t *testing.T) *fakeServer {
	t.Helper()

	fs := &fakeServer{
		t:          t,
		fromDaemon: make(chan json.RawMessage, 100),
		toDaemon:   make(chan []byte, 100),
	}

	mux := http.NewServeMux()
	handleConnect := func(w http.ResponseWriter, r *http.Request) {
		conn, err := upgrader.Upgrade(w, r, nil)
		if err != nil {
			t.Logf("fakeServer: upgrade error: %v", err)
			return
		}

		fs.mu.Lock()
		fs.conn = conn
		fs.mu.Unlock()

		// Write pump: relay toDaemon messages to the connection.
		go func() {
			for data := range fs.toDaemon {
				if err := conn.WriteMessage(websocket.TextMessage, data); err != nil {
					return
				}
			}
		}()

		// Read pump: capture all daemon messages.
		for {
			_, data, err := conn.ReadMessage()
			if err != nil {
				return
			}
			select {
			case fs.fromDaemon <- json.RawMessage(data):
			default:
				t.Logf("fakeServer: fromDaemon channel full, dropping message")
			}
		}
	}

	mux.HandleFunc("/api/v1/workspaces/"+fakeWorkspaceID+"/daemon/connect", handleConnect)
	// Also serve a simple agent runtime endpoint for outbound tests.
	mux.HandleFunc("/api/v1/workspaces/"+fakeWorkspaceID+"/agents/", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(`{"messageId":"msg-fake-001"}`))
	})

	fs.srv = httptest.NewServer(mux)
	t.Cleanup(func() {
		close(fs.toDaemon)
		fs.mu.Lock()
		if fs.conn != nil {
			_ = fs.conn.Close()
		}
		fs.mu.Unlock()
		fs.srv.Close()
	})
	return fs
}

// URL returns the server's base URL (http://...).
func (fs *fakeServer) URL() string {
	return fs.srv.URL
}

// Send sends a JSON message from the server to the daemon.
func (fs *fakeServer) Send(v any) {
	fs.t.Helper()
	data, err := json.Marshal(v)
	if err != nil {
		fs.t.Fatalf("fakeServer.Send: marshal: %v", err)
	}
	fs.toDaemon <- data
}

// SendRaw sends raw bytes from the server to the daemon.
func (fs *fakeServer) SendRaw(data []byte) {
	fs.toDaemon <- data
}

// ---------------------------------------------------------------------------
// Message reading helpers
// ---------------------------------------------------------------------------

// readMsg reads the next message from the daemon, blocking up to timeout.
func (fs *fakeServer) readMsg(timeout time.Duration) (json.RawMessage, error) {
	select {
	case msg := <-fs.fromDaemon:
		return msg, nil
	case <-time.After(timeout):
		return nil, fmt.Errorf("readMsg: timeout after %s", timeout)
	}
}

// readUntilType reads messages until one with the given "type" field is found.
func (fs *fakeServer) readUntilType(msgType string, timeout time.Duration) (json.RawMessage, error) {
	fs.t.Helper()
	deadline := time.Now().Add(timeout)
	for {
		remaining := time.Until(deadline)
		if remaining <= 0 {
			return nil, fmt.Errorf("readUntilType(%q): timeout after %s", msgType, timeout)
		}
		msg, err := fs.readMsg(remaining)
		if err != nil {
			return nil, fmt.Errorf("readUntilType(%q): %w", msgType, err)
		}
		var env struct {
			Type string `json:"type"`
		}
		if json.Unmarshal(msg, &env) == nil && env.Type == msgType {
			return msg, nil
		}
	}
}

// mustReadUntilType is like readUntilType but calls t.Fatalf on failure.
func (fs *fakeServer) mustReadUntilType(msgType string, timeout time.Duration) json.RawMessage {
	fs.t.Helper()
	msg, err := fs.readUntilType(msgType, timeout)
	if err != nil {
		fs.t.Fatalf("%v", err)
	}
	return msg
}

// drainReady reads and discards messages until the "ready" message is found.
func (fs *fakeServer) drainReady() {
	fs.t.Helper()
	fs.mustReadUntilType("ready", 5*time.Second)
}

// drainUntil reads and discards messages until one of the given type is found.
// It also drains any agent:list messages that occur during startup.
func (fs *fakeServer) drainUntil(msgType string) json.RawMessage {
	fs.t.Helper()
	return fs.mustReadUntilType(msgType, 5*time.Second)
}

// ---------------------------------------------------------------------------
// Polling helper
// ---------------------------------------------------------------------------

// waitFor polls fn at interval until it returns true or ctx expires.
func waitFor(ctx context.Context, interval time.Duration, fn func(context.Context) (bool, error)) error {
	ticker := time.NewTicker(interval)
	defer ticker.Stop()
	for {
		ok, err := fn(ctx)
		if err != nil {
			return err
		}
		if ok {
			return nil
		}
		select {
		case <-ctx.Done():
			return fmt.Errorf("waitFor: %w", ctx.Err())
		case <-ticker.C:
		}
	}
}

// ---------------------------------------------------------------------------
// JSON decode helpers
// ---------------------------------------------------------------------------

// decodeType extracts the "type" field from a JSON message.
func decodeType(raw json.RawMessage) string {
	var env struct {
		Type string `json:"type"`
	}
	_ = json.Unmarshal(raw, &env)
	return env.Type
}

// decodeField extracts a string field from a JSON message.
func decodeField(raw json.RawMessage, field string) string {
	var m map[string]json.RawMessage
	if err := json.Unmarshal(raw, &m); err != nil {
		return ""
	}
	v, ok := m[field]
	if !ok {
		return ""
	}
	var s string
	if json.Unmarshal(v, &s) == nil {
		return s
	}
	return string(v)
}

// decodeIntField extracts a numeric field from a JSON message.
func decodeIntField(raw json.RawMessage, field string) int {
	var m map[string]json.RawMessage
	if err := json.Unmarshal(raw, &m); err != nil {
		return 0
	}
	v, ok := m[field]
	if !ok {
		return 0
	}
	var n float64
	if json.Unmarshal(v, &n) == nil {
		return int(n)
	}
	return 0
}

// decodeBoolField extracts a boolean field from a JSON message.
func decodeBoolField(raw json.RawMessage, field string) bool {
	var m map[string]json.RawMessage
	if err := json.Unmarshal(raw, &m); err != nil {
		return false
	}
	v, ok := m[field]
	if !ok {
		return false
	}
	var b bool
	if json.Unmarshal(v, &b) == nil {
		return b
	}
	return false
}
