package opencode

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

	"github.com/orchard9/rdev/internal/domain"
	"github.com/orchard9/rdev/internal/port"
)

// Ensure Adapter implements port.CodeAgent at compile time.
var _ port.CodeAgent = (*Adapter)(nil)

func TestAdapter_Name(t *testing.T) {
	adapter := NewAdapter(ClientConfig{})
	if name := adapter.Name(); name != "OpenCode" {
		t.Errorf("expected name 'OpenCode', got %q", name)
	}
}

func TestAdapter_Provider(t *testing.T) {
	adapter := NewAdapter(ClientConfig{})
	if p := adapter.Provider(); p != domain.AgentProviderOpenCode {
		t.Errorf("expected provider 'opencode', got %q", p)
	}
}

func TestAdapter_Capabilities(t *testing.T) {
	adapter := NewAdapter(ClientConfig{})
	caps := adapter.Capabilities()

	if caps.Provider != domain.AgentProviderOpenCode {
		t.Errorf("expected provider opencode")
	}
	if !caps.SupportsSessionContinuation {
		t.Error("expected session continuation support")
	}
	if !caps.SupportsModelSelection {
		t.Error("expected model selection support")
	}
	if !caps.SupportsToolControl {
		t.Error("expected tool control support")
	}
	if !caps.SupportsStreaming {
		t.Error("expected streaming support")
	}
	if len(caps.SupportedModels) == 0 {
		t.Error("expected at least one supported model")
	}
}

func TestAdapter_Execute_MissingPrompt(t *testing.T) {
	adapter := NewAdapter(ClientConfig{})
	req := &domain.AgentRequest{
		Prompt: "", // missing
	}

	_, err := adapter.Execute(context.Background(), req, func(e domain.AgentEvent) {})
	if err == nil {
		t.Error("expected error for missing prompt")
	}
}

func TestAdapter_Available_Healthy(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/global/health" {
			json.NewEncoder(w).Encode(HealthResponse{Healthy: true, Version: "1.0.0"})
			return
		}
		http.NotFound(w, r)
	}))
	defer server.Close()

	adapter := NewAdapter(ClientConfig{BaseURL: server.URL})
	if !adapter.Available(context.Background()) {
		t.Error("expected adapter to be available")
	}
}

func TestAdapter_Available_Unhealthy(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/global/health" {
			json.NewEncoder(w).Encode(HealthResponse{Healthy: false})
			return
		}
		http.NotFound(w, r)
	}))
	defer server.Close()

	adapter := NewAdapter(ClientConfig{BaseURL: server.URL})
	if adapter.Available(context.Background()) {
		t.Error("expected adapter to be unavailable")
	}
}

func TestAdapter_Available_ServerDown(t *testing.T) {
	adapter := NewAdapter(ClientConfig{BaseURL: "http://localhost:59999"})
	if adapter.Available(context.Background()) {
		t.Error("expected adapter to be unavailable when server is down")
	}
}

func TestAdapter_Execute_WithMockServer(t *testing.T) {
	sessionID := "test-session-123"

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch {
		case r.URL.Path == "/session" && r.Method == http.MethodPost:
			// Create session
			json.NewEncoder(w).Encode(Session{ID: sessionID})

		case r.URL.Path == "/session/"+sessionID+"/message" && r.Method == http.MethodPost:
			// Send message response
			json.NewEncoder(w).Encode(SendMessageResponse{
				Info: MessageInfo{ID: "msg-1", Role: "assistant"},
				Parts: []MessagePart{
					{Type: "text", Content: "Hello from OpenCode!"},
				},
			})

		case r.URL.Path == "/event":
			// SSE endpoint - just close immediately for this test
			w.WriteHeader(http.StatusOK)
			return

		default:
			http.NotFound(w, r)
		}
	}))
	defer server.Close()

	adapter := NewAdapter(ClientConfig{BaseURL: server.URL})

	var events []domain.AgentEvent
	handler := func(e domain.AgentEvent) {
		events = append(events, e)
	}

	req := &domain.AgentRequest{
		Prompt: "Say hello",
	}

	result, err := adapter.Execute(context.Background(), req, handler)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if result.SessionID != sessionID {
		t.Errorf("expected session ID %q, got %q", sessionID, result.SessionID)
	}
	if result.ExitCode != 0 {
		t.Errorf("expected exit code 0, got %d", result.ExitCode)
	}
	if result.FinalOutput != "Hello from OpenCode!" {
		t.Errorf("expected final output 'Hello from OpenCode!', got %q", result.FinalOutput)
	}

	// Should have at least session started + output + complete events
	if len(events) < 3 {
		t.Errorf("expected at least 3 events, got %d", len(events))
	}
}

func TestAdapter_Execute_WithModel(t *testing.T) {
	var receivedModel string

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch {
		case r.URL.Path == "/session" && r.Method == http.MethodPost:
			json.NewEncoder(w).Encode(Session{ID: "sess-1"})

		case r.URL.Path == "/session/sess-1/message" && r.Method == http.MethodPost:
			var req SendMessageRequest
			json.NewDecoder(r.Body).Decode(&req)
			receivedModel = req.Model

			json.NewEncoder(w).Encode(SendMessageResponse{
				Info:  MessageInfo{ID: "msg-1"},
				Parts: []MessagePart{{Type: "text", Content: "Done"}},
			})

		case r.URL.Path == "/event":
			w.WriteHeader(http.StatusOK)
			return

		default:
			http.NotFound(w, r)
		}
	}))
	defer server.Close()

	adapter := NewAdapter(ClientConfig{BaseURL: server.URL})

	req := &domain.AgentRequest{
		Prompt: "Test",
		Model:  "gpt-4o",
	}

	_, err := adapter.Execute(context.Background(), req, func(e domain.AgentEvent) {})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if receivedModel != "gpt-4o" {
		t.Errorf("expected model 'gpt-4o', got %q", receivedModel)
	}
}

func TestAdapter_Cancel(t *testing.T) {
	abortCalled := false

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/session/test-session/abort" && r.Method == http.MethodPost {
			abortCalled = true
			w.WriteHeader(http.StatusOK)
			return
		}
		http.NotFound(w, r)
	}))
	defer server.Close()

	adapter := NewAdapter(ClientConfig{BaseURL: server.URL})

	err := adapter.Cancel(context.Background(), "test-session")
	if err != nil {
		t.Errorf("unexpected error: %v", err)
	}

	if !abortCalled {
		t.Error("expected abort endpoint to be called")
	}
}

func TestAdapter_Execute_WithErrorPart(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch {
		case r.URL.Path == "/session" && r.Method == http.MethodPost:
			json.NewEncoder(w).Encode(Session{ID: "sess-err"})

		case r.URL.Path == "/session/sess-err/message" && r.Method == http.MethodPost:
			// Return response with error part
			json.NewEncoder(w).Encode(SendMessageResponse{
				Info: MessageInfo{ID: "msg-1"},
				Parts: []MessagePart{
					{Type: "text", Content: "Attempting task..."},
					{Type: "error", Content: "Command failed with exit code 1"},
				},
			})

		case r.URL.Path == "/event":
			w.WriteHeader(http.StatusOK)
			return

		default:
			http.NotFound(w, r)
		}
	}))
	defer server.Close()

	adapter := NewAdapter(ClientConfig{BaseURL: server.URL})

	req := &domain.AgentRequest{
		Prompt: "Run failing command",
	}

	result, err := adapter.Execute(context.Background(), req, func(e domain.AgentEvent) {})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	// Should have non-zero exit code when error part is present
	if result.ExitCode != 1 {
		t.Errorf("expected exit code 1 for error response, got %d", result.ExitCode)
	}

	// Should have error set
	if result.Error == nil {
		t.Error("expected error to be set when error part is present")
	}

	// Error should contain the error content
	if result.Error != nil && !strings.Contains(result.Error.Error(), "Command failed") {
		t.Errorf("expected error to contain 'Command failed', got %q", result.Error.Error())
	}
}

func TestAdapter_partToEvent(t *testing.T) {
	adapter := NewAdapter(ClientConfig{})

	tests := []struct {
		name      string
		part      MessagePart
		wantType  domain.AgentEventType
		wantValue string
	}{
		{
			name:      "text part",
			part:      MessagePart{Type: "text", Content: "Hello"},
			wantType:  domain.AgentEventOutput,
			wantValue: "Hello",
		},
		{
			name:      "tool_use part",
			part:      MessagePart{Type: "tool_use", Name: "Bash"},
			wantType:  domain.AgentEventToolUse,
			wantValue: "Bash",
		},
		{
			name:      "tool_result part",
			part:      MessagePart{Type: "tool_result", Content: "output here"},
			wantType:  domain.AgentEventToolResult,
			wantValue: "output here",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			event := adapter.partToEvent(tt.part)
			if event.Type != tt.wantType {
				t.Errorf("expected type %v, got %v", tt.wantType, event.Type)
			}
			if event.Content != tt.wantValue {
				t.Errorf("expected content %q, got %q", tt.wantValue, event.Content)
			}
		})
	}
}

func TestAdapter_sseToEvent(t *testing.T) {
	adapter := NewAdapter(ClientConfig{})

	tests := []struct {
		name     string
		sse      SSEEvent
		wantType domain.AgentEventType
	}{
		{
			name:     "server connected",
			sse:      SSEEvent{Event: "server.connected"},
			wantType: domain.AgentEventOutput,
		},
		{
			name:     "tool started",
			sse:      SSEEvent{Event: "tool.started", Data: `{"name":"Bash"}`},
			wantType: domain.AgentEventToolUse,
		},
		{
			name:     "tool completed",
			sse:      SSEEvent{Event: "tool.completed", Data: `{"output":"done"}`},
			wantType: domain.AgentEventToolResult,
		},
		{
			name:     "session completed",
			sse:      SSEEvent{Event: "session.completed"},
			wantType: domain.AgentEventComplete,
		},
		{
			name:     "error",
			sse:      SSEEvent{Event: "error", Data: "something failed"},
			wantType: domain.AgentEventError,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			event := adapter.sseToEvent(tt.sse)
			if event.Type != tt.wantType {
				t.Errorf("expected type %v, got %v", tt.wantType, event.Type)
			}
		})
	}
}

func TestClient_Health(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/global/health" {
			json.NewEncoder(w).Encode(HealthResponse{Healthy: true, Version: "1.2.3"})
			return
		}
		http.NotFound(w, r)
	}))
	defer server.Close()

	client := NewClient(ClientConfig{BaseURL: server.URL})
	health, err := client.Health(context.Background())

	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if !health.Healthy {
		t.Error("expected healthy=true")
	}
	if health.Version != "1.2.3" {
		t.Errorf("expected version '1.2.3', got %q", health.Version)
	}
}

func TestClient_CreateSession(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/session" && r.Method == http.MethodPost {
			var req CreateSessionRequest
			json.NewDecoder(r.Body).Decode(&req)

			json.NewEncoder(w).Encode(Session{
				ID:        "new-session-id",
				Title:     req.Title,
				CreatedAt: time.Now(),
			})
			return
		}
		http.NotFound(w, r)
	}))
	defer server.Close()

	client := NewClient(ClientConfig{BaseURL: server.URL})
	session, err := client.CreateSession(context.Background(), &CreateSessionRequest{
		Title: "Test Session",
	})

	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if session.ID != "new-session-id" {
		t.Errorf("expected ID 'new-session-id', got %q", session.ID)
	}
	if session.Title != "Test Session" {
		t.Errorf("expected title 'Test Session', got %q", session.Title)
	}
}

func TestClient_WithAuth(t *testing.T) {
	var authHeader string

	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		authHeader = r.Header.Get("Authorization")
		json.NewEncoder(w).Encode(HealthResponse{Healthy: true})
	}))
	defer server.Close()

	client := NewClient(ClientConfig{
		BaseURL:  server.URL,
		Username: "opencode",
		Password: "secret",
	})

	_, err := client.Health(context.Background())
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}

	if authHeader == "" {
		t.Error("expected Authorization header to be set")
	}
	if authHeader != "Basic b3BlbmNvZGU6c2VjcmV0" {
		t.Errorf("unexpected auth header: %s", authHeader)
	}
}
