package handler

import (
	"encoding/json"
	"errors"
	"io"
	"log"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"

	"github.com/gorilla/mux"

	"github.com/seifghazi/claude-code-monitor/internal/service"
)

type conversationServiceStub struct {
	getConversationsFn          func() (map[string][]*service.Conversation, error)
	getConversationFn           func(projectPath, sessionID string) (*service.Conversation, error)
	getConversationsByProjectFn func(projectPath string) ([]*service.Conversation, error)
}

func (s *conversationServiceStub) GetConversations() (map[string][]*service.Conversation, error) {
	if s.getConversationsFn != nil {
		return s.getConversationsFn()
	}
	panic("unexpected call")
}

func (s *conversationServiceStub) GetConversation(projectPath, sessionID string) (*service.Conversation, error) {
	if s.getConversationFn != nil {
		return s.getConversationFn(projectPath, sessionID)
	}
	panic("unexpected call")
}

func (s *conversationServiceStub) GetConversationsByProject(projectPath string) ([]*service.Conversation, error) {
	if s.getConversationsByProjectFn != nil {
		return s.getConversationsByProjectFn(projectPath)
	}
	panic("unexpected call")
}

func newConversationHandler(stub *conversationServiceStub) *Handler {
	return &Handler{
		conversationService: stub,
		logger:              log.New(io.Discard, "", 0),
	}
}

func rawMessage(t *testing.T, v interface{}) json.RawMessage {
	t.Helper()

	data, err := json.Marshal(v)
	if err != nil {
		t.Fatalf("failed to marshal test raw message: %v", err)
	}
	return data
}

func TestConversationModelMatchesFilter(t *testing.T) {
	tests := []struct {
		name      string
		model     string
		filter    string
		wantMatch bool
	}{
		{name: "all matches any model", model: "claude-opus-4-6", filter: "all", wantMatch: true},
		{name: "opus matches opus tier", model: "claude-opus-4-6", filter: "opus", wantMatch: true},
		{name: "sonnet does not match opus tier", model: "claude-sonnet-4-5", filter: "opus", wantMatch: false},
		{name: "empty model does not match tier filter", model: "", filter: "haiku", wantMatch: false},
		{name: "substring filter still works", model: "claude-opus-4-6", filter: "opus-4", wantMatch: true},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := conversationModelMatchesFilter(tt.model, tt.filter); got != tt.wantMatch {
				t.Fatalf("conversationModelMatchesFilter(%q, %q) = %v, want %v", tt.model, tt.filter, got, tt.wantMatch)
			}
		})
	}
}

func TestGetConversationsAppliesFilterSortAndPagination(t *testing.T) {
	oldest := time.Date(2026, 3, 18, 9, 0, 0, 0, time.UTC)
	middle := time.Date(2026, 3, 19, 9, 0, 0, 0, time.UTC)
	newest := time.Date(2026, 3, 20, 9, 0, 0, 0, time.UTC)

	stub := &conversationServiceStub{
		getConversationsFn: func() (map[string][]*service.Conversation, error) {
			return map[string][]*service.Conversation{
				"proj-a": {
					{
						SessionID:    "old-opus",
						ProjectPath:  "proj-a",
						ProjectName:  "Proj A",
						Model:        "claude-opus-4-6",
						StartTime:    oldest.Add(-5 * time.Minute),
						EndTime:      oldest,
						MessageCount: 1,
						Messages: []*service.ConversationMessage{
							{Type: "user", Message: rawMessage(t, "short prompt")},
						},
					},
					{
						SessionID:    "new-sonnet",
						ProjectPath:  "proj-a",
						ProjectName:  "Proj A",
						Model:        "claude-sonnet-4-5",
						StartTime:    newest.Add(-2 * time.Minute),
						EndTime:      newest,
						MessageCount: 2,
						Messages: []*service.ConversationMessage{
							{Type: "user", Message: rawMessage(t, []map[string]string{{"type": "text", "text": "newest prompt"}})},
						},
					},
				},
				"proj-b": {
					{
						SessionID:    "mid-opus",
						ProjectPath:  "proj-b",
						ProjectName:  "Proj B",
						Model:        "claude-opus-4-6",
						StartTime:    middle.Add(-3 * time.Minute),
						EndTime:      middle,
						MessageCount: 3,
						Messages: []*service.ConversationMessage{
							{Type: "user", Message: rawMessage(t, map[string]string{"content": "middle prompt"})},
						},
					},
				},
			}, nil
		},
	}

	req := httptest.NewRequest(http.MethodGet, "/api/conversations?model=opus&page=1&limit=1", nil)
	rr := httptest.NewRecorder()

	newConversationHandler(stub).GetConversations(rr, req)

	if rr.Code != http.StatusOK {
		t.Fatalf("expected 200, got %d", rr.Code)
	}

	var response struct {
		Conversations []map[string]interface{} `json:"conversations"`
		HasMore       bool                     `json:"hasMore"`
		Total         int                      `json:"total"`
		Page          int                      `json:"page"`
		Limit         int                      `json:"limit"`
	}
	decodeJSONBody(t, rr, &response)

	if response.Total != 2 || !response.HasMore || response.Page != 1 || response.Limit != 1 {
		t.Fatalf("unexpected pagination metadata: %#v", response)
	}
	if len(response.Conversations) != 1 {
		t.Fatalf("expected one paginated conversation, got %d", len(response.Conversations))
	}

	first := response.Conversations[0]
	if first["id"] != "mid-opus" {
		t.Fatalf("expected newest matching opus conversation first, got %#v", first)
	}
	if first["firstMessage"] != "middle prompt" {
		t.Fatalf("expected extracted first message, got %#v", first["firstMessage"])
	}
}

func TestGetConversationsReturnsInternalServerErrorOnServiceFailure(t *testing.T) {
	stub := &conversationServiceStub{
		getConversationsFn: func() (map[string][]*service.Conversation, error) {
			return nil, errors.New("boom")
		},
	}
	req := httptest.NewRequest(http.MethodGet, "/api/conversations", nil)
	rr := httptest.NewRecorder()

	newConversationHandler(stub).GetConversations(rr, req)

	if rr.Code != http.StatusInternalServerError {
		t.Fatalf("expected 500, got %d", rr.Code)
	}

	var response struct {
		Error string `json:"error"`
	}
	decodeJSONBody(t, rr, &response)
	if response.Error != "Failed to get conversations" {
		t.Fatalf("unexpected error response: %#v", response)
	}
}

func TestGetConversationByIDRequiresProjectAndReturnsConversation(t *testing.T) {
	t.Run("missing project", func(t *testing.T) {
		req := mux.SetURLVars(httptest.NewRequest(http.MethodGet, "/api/conversations/session-1", nil), map[string]string{"id": "session-1"})
		rr := httptest.NewRecorder()

		newConversationHandler(&conversationServiceStub{}).GetConversationByID(rr, req)

		if rr.Code != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", rr.Code)
		}

		var response struct {
			Error string `json:"error"`
		}
		decodeJSONBody(t, rr, &response)
		if response.Error != "Project path is required" {
			t.Fatalf("unexpected error response: %#v", response)
		}
	})

	t.Run("success", func(t *testing.T) {
		stub := &conversationServiceStub{
			getConversationFn: func(projectPath, sessionID string) (*service.Conversation, error) {
				if projectPath != "team/app" || sessionID != "session-1" {
					t.Fatalf("unexpected conversation lookup %q %q", projectPath, sessionID)
				}
				return &service.Conversation{
					SessionID:    "session-1",
					ProjectPath:  "team/app",
					ProjectName:  "app",
					Model:        "claude-opus-4-6",
					StartTime:    time.Date(2026, 3, 20, 8, 0, 0, 0, time.UTC),
					EndTime:      time.Date(2026, 3, 20, 8, 5, 0, 0, time.UTC),
					MessageCount: 2,
				}, nil
			},
		}
		req := mux.SetURLVars(httptest.NewRequest(http.MethodGet, "/api/conversations/session-1?project=team/app", nil), map[string]string{"id": "session-1"})
		rr := httptest.NewRecorder()

		newConversationHandler(stub).GetConversationByID(rr, req)

		if rr.Code != http.StatusOK {
			t.Fatalf("expected 200, got %d", rr.Code)
		}

		var response service.Conversation
		decodeJSONBody(t, rr, &response)
		if response.SessionID != "session-1" || response.ProjectPath != "team/app" {
			t.Fatalf("unexpected conversation payload: %#v", response)
		}
	})
}

func TestGetConversationsByProjectRequiresProjectAndHandlesFailure(t *testing.T) {
	t.Run("missing project", func(t *testing.T) {
		req := httptest.NewRequest(http.MethodGet, "/api/conversations/project", nil)
		rr := httptest.NewRecorder()

		newConversationHandler(&conversationServiceStub{}).GetConversationsByProject(rr, req)

		if rr.Code != http.StatusBadRequest {
			t.Fatalf("expected 400, got %d", rr.Code)
		}
	})

	t.Run("service failure", func(t *testing.T) {
		stub := &conversationServiceStub{
			getConversationsByProjectFn: func(projectPath string) ([]*service.Conversation, error) {
				if projectPath != "team/app" {
					t.Fatalf("unexpected project path %q", projectPath)
				}
				return nil, errors.New("boom")
			},
		}
		req := httptest.NewRequest(http.MethodGet, "/api/conversations/project?project=team/app", nil)
		rr := httptest.NewRecorder()

		newConversationHandler(stub).GetConversationsByProject(rr, req)

		if rr.Code != http.StatusInternalServerError {
			t.Fatalf("expected 500, got %d", rr.Code)
		}

		var response struct {
			Error string `json:"error"`
		}
		decodeJSONBody(t, rr, &response)
		if response.Error != "Failed to get project conversations" {
			t.Fatalf("unexpected error response: %#v", response)
		}
	})
}
