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/config"
	"github.com/seifghazi/claude-code-monitor/internal/model"
)

type dashboardStorageStub struct {
	getRequestsFn                 func(page, limit int, modelFilter string) ([]model.RequestLog, int, error)
	getUsageStatsFn               func(startDate, endDate, modelFilter, orgFilter string) (*model.UsageStats, error)
	getRequestsSummaryPaginatedFn func(modelFilter, startTime, endTime string, offset, limit int) ([]*model.RequestSummary, int, error)
	getRequestByShortIDFn         func(shortID string) (*model.RequestLog, string, error)
	getStatsFn                    func(startDate, endDate, orgFilter string) (*model.DashboardStats, error)
	getHourlyStatsFn              func(startTime, endTime string, bucketMinutes int, orgFilter string) (*model.HourlyStatsResponse, error)
	getModelStatsFn               func(startTime, endTime, orgFilter string) (*model.ModelStatsResponse, error)
	getDistinctOrganizationsFn    func() ([]string, error)
	getLatestRequestDateFn        func() (*time.Time, error)
	getSettingsFn                 func() (*model.ProxySettings, error)
	saveSettingsFn                func(settings *model.ProxySettings) error
	clearRequestsFn               func() (int, error)
}

func (s *dashboardStorageStub) SaveRequest(*model.RequestLog) (string, error) {
	panic("unexpected call")
}
func (s *dashboardStorageStub) GetRequests(page, limit int, modelFilter string) ([]model.RequestLog, int, error) {
	if s.getRequestsFn != nil {
		return s.getRequestsFn(page, limit, modelFilter)
	}
	panic("unexpected call")
}
func (s *dashboardStorageStub) GetAllRequests(string) ([]*model.RequestLog, error) {
	panic("unexpected call")
}
func (s *dashboardStorageStub) GetRequestByShortID(shortID string) (*model.RequestLog, string, error) {
	if s.getRequestByShortIDFn != nil {
		return s.getRequestByShortIDFn(shortID)
	}
	panic("unexpected call")
}
func (s *dashboardStorageStub) ClearRequests() (int, error) {
	if s.clearRequestsFn != nil {
		return s.clearRequestsFn()
	}
	panic("unexpected call")
}
func (s *dashboardStorageStub) UpdateRequestWithGrading(string, *model.PromptGrade) error {
	panic("unexpected call")
}
func (s *dashboardStorageStub) UpdateRequestWithResponse(*model.RequestLog) error {
	panic("unexpected call")
}
func (s *dashboardStorageStub) DeleteRequestsOlderThan(time.Duration) (int, error) {
	panic("unexpected call")
}
func (s *dashboardStorageStub) GetDatabaseStats() (map[string]interface{}, error) {
	panic("unexpected call")
}
func (s *dashboardStorageStub) GetUsageStats(startDate, endDate, modelFilter, orgFilter string) (*model.UsageStats, error) {
	if s.getUsageStatsFn != nil {
		return s.getUsageStatsFn(startDate, endDate, modelFilter, orgFilter)
	}
	panic("unexpected call")
}
func (s *dashboardStorageStub) GetRequestsSummary(string) ([]*model.RequestSummary, error) {
	panic("unexpected call")
}
func (s *dashboardStorageStub) GetRequestsSummaryPaginated(modelFilter, startTime, endTime string, offset, limit int) ([]*model.RequestSummary, int, error) {
	if s.getRequestsSummaryPaginatedFn != nil {
		return s.getRequestsSummaryPaginatedFn(modelFilter, startTime, endTime, offset, limit)
	}
	panic("unexpected call")
}
func (s *dashboardStorageStub) GetStats(startDate, endDate, orgFilter string) (*model.DashboardStats, error) {
	if s.getStatsFn != nil {
		return s.getStatsFn(startDate, endDate, orgFilter)
	}
	panic("unexpected call")
}
func (s *dashboardStorageStub) GetHourlyStats(startTime, endTime string, bucketMinutes int, orgFilter string) (*model.HourlyStatsResponse, error) {
	if s.getHourlyStatsFn != nil {
		return s.getHourlyStatsFn(startTime, endTime, bucketMinutes, orgFilter)
	}
	panic("unexpected call")
}
func (s *dashboardStorageStub) GetModelStats(startTime, endTime, orgFilter string) (*model.ModelStatsResponse, error) {
	if s.getModelStatsFn != nil {
		return s.getModelStatsFn(startTime, endTime, orgFilter)
	}
	panic("unexpected call")
}
func (s *dashboardStorageStub) GetLatestRequestDate() (*time.Time, error) {
	if s.getLatestRequestDateFn != nil {
		return s.getLatestRequestDateFn()
	}
	panic("unexpected call")
}
func (s *dashboardStorageStub) GetDistinctOrganizations() ([]string, error) {
	if s.getDistinctOrganizationsFn != nil {
		return s.getDistinctOrganizationsFn()
	}
	panic("unexpected call")
}
func (s *dashboardStorageStub) GetSettings() (*model.ProxySettings, error) {
	if s.getSettingsFn != nil {
		return s.getSettingsFn()
	}
	return &model.ProxySettings{}, nil
}
func (s *dashboardStorageStub) SaveSettings(settings *model.ProxySettings) error {
	if s.saveSettingsFn != nil {
		return s.saveSettingsFn(settings)
	}
	return nil
}
func (s *dashboardStorageStub) GetConfig() *config.StorageConfig { return &config.StorageConfig{} }
func (s *dashboardStorageStub) EnsureDirectoryExists() error     { return nil }
func (s *dashboardStorageStub) Close() error                     { return nil }

func newTestHandler(storage *dashboardStorageStub) *Handler {
	return &Handler{
		storageService: storage,
		logger:         log.New(io.Discard, "", 0),
	}
}

func decodeJSONBody(t *testing.T, rr *httptest.ResponseRecorder, dest interface{}) {
	t.Helper()

	if err := json.NewDecoder(rr.Body).Decode(dest); err != nil {
		t.Fatalf("failed decoding JSON response: %v", err)
	}
}

func TestGetRequestsUsesDefaultPaginationAndModelFilter(t *testing.T) {
	storage := &dashboardStorageStub{
		getRequestsFn: func(page, limit int, modelFilter string) ([]model.RequestLog, int, error) {
			if page != defaultPage {
				t.Fatalf("expected page %d, got %d", defaultPage, page)
			}
			if limit != defaultPageLimit {
				t.Fatalf("expected limit %d, got %d", defaultPageLimit, limit)
			}
			if modelFilter != "all" {
				t.Fatalf("expected model filter all, got %q", modelFilter)
			}

			return []model.RequestLog{{RequestID: "req-1"}}, 7, nil
		},
	}
	req := httptest.NewRequest(http.MethodGet, "/api/requests", nil)
	rr := httptest.NewRecorder()

	newTestHandler(storage).GetRequests(rr, req)

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

	var response struct {
		Requests []model.RequestLog `json:"requests"`
		Total    int                `json:"total"`
	}
	decodeJSONBody(t, rr, &response)

	if len(response.Requests) != 1 || response.Requests[0].RequestID != "req-1" {
		t.Fatalf("unexpected requests payload: %#v", response.Requests)
	}
	if response.Total != 7 {
		t.Fatalf("expected total 7, got %d", response.Total)
	}
}

func TestGetRequestsReturnsInternalServerErrorOnStorageFailure(t *testing.T) {
	storage := &dashboardStorageStub{
		getRequestsFn: func(page, limit int, modelFilter string) ([]model.RequestLog, int, error) {
			return nil, 0, errors.New("boom")
		},
	}
	req := httptest.NewRequest(http.MethodGet, "/api/requests?page=2&limit=25&model=opus", nil)
	rr := httptest.NewRecorder()

	newTestHandler(storage).GetRequests(rr, req)

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

	var response model.ErrorResponse
	decodeJSONBody(t, rr, &response)
	if response.Error != "Failed to get requests" {
		t.Fatalf("unexpected error response: %#v", response)
	}
}

func TestDeleteRequestsReturnsDeletedCountAndStorageErrors(t *testing.T) {
	t.Run("success", func(t *testing.T) {
		storage := &dashboardStorageStub{
			clearRequestsFn: func() (int, error) {
				return 12, nil
			},
		}
		req := httptest.NewRequest(http.MethodDelete, "/api/requests", nil)
		rr := httptest.NewRecorder()

		newTestHandler(storage).DeleteRequests(rr, req)

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

		var response struct {
			Message string `json:"message"`
			Deleted int    `json:"deleted"`
		}
		decodeJSONBody(t, rr, &response)
		if response.Message != "Request history cleared" || response.Deleted != 12 {
			t.Fatalf("unexpected delete response: %#v", response)
		}
	})

	t.Run("storage error", func(t *testing.T) {
		storage := &dashboardStorageStub{
			clearRequestsFn: func() (int, error) {
				return 0, errors.New("boom")
			},
		}
		req := httptest.NewRequest(http.MethodDelete, "/api/requests", nil)
		rr := httptest.NewRecorder()

		newTestHandler(storage).DeleteRequests(rr, req)

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

		var response model.ErrorResponse
		decodeJSONBody(t, rr, &response)
		if response.Error != "Error clearing request history" {
			t.Fatalf("unexpected error response: %#v", response)
		}
	})
}

func TestGetRequestsSummaryNormalizesPaginationInputs(t *testing.T) {
	storage := &dashboardStorageStub{
		getRequestsSummaryPaginatedFn: func(modelFilter, startTime, endTime string, offset, limit int) ([]*model.RequestSummary, int, error) {
			if modelFilter != "all" {
				t.Fatalf("expected default model filter all, got %q", modelFilter)
			}
			if startTime != "2026-03-01T00:00:00Z" {
				t.Fatalf("unexpected start time %q", startTime)
			}
			if endTime != "2026-03-02T00:00:00Z" {
				t.Fatalf("unexpected end time %q", endTime)
			}
			if offset != 0 {
				t.Fatalf("expected invalid negative offset to normalize to 0, got %d", offset)
			}
			if limit != 0 {
				t.Fatalf("expected invalid oversize limit to normalize to 0, got %d", limit)
			}

			return []*model.RequestSummary{{RequestID: "req-summary-1"}}, 1, nil
		},
	}
	req := httptest.NewRequest(http.MethodGet, "/api/requests/summary?start=2026-03-01T00:00:00Z&end=2026-03-02T00:00:00Z&offset=-4&limit=100001", nil)
	rr := httptest.NewRecorder()

	newTestHandler(storage).GetRequestsSummary(rr, req)

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

	var response struct {
		Requests []*model.RequestSummary `json:"requests"`
		Total    int                     `json:"total"`
		Offset   int                     `json:"offset"`
		Limit    int                     `json:"limit"`
	}
	decodeJSONBody(t, rr, &response)

	if len(response.Requests) != 1 || response.Requests[0].RequestID != "req-summary-1" {
		t.Fatalf("unexpected summaries payload: %#v", response.Requests)
	}
	if response.Total != 1 || response.Offset != 0 || response.Limit != 0 {
		t.Fatalf("unexpected summary metadata: %#v", response)
	}
}

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

		newTestHandler(&dashboardStorageStub{}).GetRequestByID(rr, req)

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

		var response model.ErrorResponse
		decodeJSONBody(t, rr, &response)
		if response.Error != "Request ID is required" {
			t.Fatalf("unexpected error response: %#v", response)
		}
	})

	t.Run("not found", func(t *testing.T) {
		storage := &dashboardStorageStub{
			getRequestByShortIDFn: func(shortID string) (*model.RequestLog, string, error) {
				if shortID != "abc123" {
					t.Fatalf("expected short ID abc123, got %q", shortID)
				}
				return nil, "", nil
			},
		}
		req := mux.SetURLVars(httptest.NewRequest(http.MethodGet, "/api/requests/abc123", nil), map[string]string{"id": "abc123"})
		rr := httptest.NewRecorder()

		newTestHandler(storage).GetRequestByID(rr, req)

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

		var response model.ErrorResponse
		decodeJSONBody(t, rr, &response)
		if response.Error != "Request not found" {
			t.Fatalf("unexpected error response: %#v", response)
		}
	})
}

func TestGetRequestByIDReturnsRequestPayloadAndStorageErrors(t *testing.T) {
	t.Run("success", func(t *testing.T) {
		storage := &dashboardStorageStub{
			getRequestByShortIDFn: func(shortID string) (*model.RequestLog, string, error) {
				if shortID != "abc123" {
					t.Fatalf("expected short ID abc123, got %q", shortID)
				}
				return &model.RequestLog{RequestID: "full-request-id", Model: "claude-opus-4-6"}, "full-request-id", nil
			},
		}
		req := mux.SetURLVars(httptest.NewRequest(http.MethodGet, "/api/requests/abc123", nil), map[string]string{"id": "abc123"})
		rr := httptest.NewRecorder()

		newTestHandler(storage).GetRequestByID(rr, req)

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

		var response struct {
			Request *model.RequestLog `json:"request"`
			FullID  string            `json:"fullId"`
		}
		decodeJSONBody(t, rr, &response)
		if response.Request == nil || response.Request.RequestID != "full-request-id" || response.FullID != "full-request-id" {
			t.Fatalf("unexpected request payload: %#v", response)
		}
	})

	t.Run("storage error", func(t *testing.T) {
		storage := &dashboardStorageStub{
			getRequestByShortIDFn: func(shortID string) (*model.RequestLog, string, error) {
				return nil, "", errors.New("boom")
			},
		}
		req := mux.SetURLVars(httptest.NewRequest(http.MethodGet, "/api/requests/abc123", nil), map[string]string{"id": "abc123"})
		rr := httptest.NewRecorder()

		newTestHandler(storage).GetRequestByID(rr, req)

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

		var response model.ErrorResponse
		decodeJSONBody(t, rr, &response)
		if response.Error != "Failed to get request" {
			t.Fatalf("unexpected error response: %#v", response)
		}
	})
}

func TestGetDashboardStatsFallsBackToLastSevenDays(t *testing.T) {
	storage := &dashboardStorageStub{
		getStatsFn: func(startDate, endDate, orgFilter string) (*model.DashboardStats, error) {
			if orgFilter != "org-1" {
				t.Fatalf("expected org filter org-1, got %q", orgFilter)
			}

			start, err := time.Parse(time.RFC3339, startDate)
			if err != nil {
				t.Fatalf("expected RFC3339 start date, got %q: %v", startDate, err)
			}
			end, err := time.Parse(time.RFC3339, endDate)
			if err != nil {
				t.Fatalf("expected RFC3339 end date, got %q: %v", endDate, err)
			}

			diff := end.Sub(start)
			if diff < (7*24*time.Hour-time.Second) || diff > (7*24*time.Hour+time.Second) {
				t.Fatalf("expected ~7 day fallback window, got %v", diff)
			}

			return &model.DashboardStats{
				DailyStats: []model.DailyTokens{{Date: "2026-03-20", Tokens: 42, Requests: 2}},
			}, nil
		},
	}
	req := httptest.NewRequest(http.MethodGet, "/api/dashboard/stats?org=org-1", nil)
	rr := httptest.NewRecorder()

	newTestHandler(storage).GetDashboardStats(rr, req)

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

	var response model.DashboardStats
	decodeJSONBody(t, rr, &response)
	if len(response.DailyStats) != 1 || response.DailyStats[0].Tokens != 42 {
		t.Fatalf("unexpected dashboard stats payload: %#v", response)
	}
}

func TestGetStatsPassesQueryFiltersThrough(t *testing.T) {
	storage := &dashboardStorageStub{
		getUsageStatsFn: func(startDate, endDate, modelFilter, orgFilter string) (*model.UsageStats, error) {
			if startDate != "2026-03-01" {
				t.Fatalf("expected start date 2026-03-01, got %q", startDate)
			}
			if endDate != "2026-03-07" {
				t.Fatalf("expected end date 2026-03-07, got %q", endDate)
			}
			if modelFilter != "claude-sonnet-4-5" {
				t.Fatalf("expected model filter claude-sonnet-4-5, got %q", modelFilter)
			}
			if orgFilter != "org-usage" {
				t.Fatalf("expected org filter org-usage, got %q", orgFilter)
			}

			return &model.UsageStats{
				TotalRequests: 3,
				RequestsByModel: map[string]model.ModelStats{
					"claude-sonnet-4-5": {RequestCount: 3},
				},
			}, nil
		},
	}
	req := httptest.NewRequest(http.MethodGet, "/api/stats?start_date=2026-03-01&end_date=2026-03-07&model=claude-sonnet-4-5&org=org-usage", nil)
	rr := httptest.NewRecorder()

	newTestHandler(storage).GetStats(rr, req)

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

	var response model.UsageStats
	decodeJSONBody(t, rr, &response)
	if response.TotalRequests != 3 {
		t.Fatalf("expected total requests 3, got %d", response.TotalRequests)
	}
	if response.RequestsByModel["claude-sonnet-4-5"].RequestCount != 3 {
		t.Fatalf("unexpected usage stats payload: %#v", response)
	}
}

func TestGetHourlyStatsValidatesRangeAndDefaultsBucket(t *testing.T) {
	t.Run("missing range", func(t *testing.T) {
		req := httptest.NewRequest(http.MethodGet, "/api/dashboard/hourly?start=2026-03-01T00:00:00Z", nil)
		rr := httptest.NewRecorder()

		newTestHandler(&dashboardStorageStub{}).GetHourlyStats(rr, req)

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

		var response model.ErrorResponse
		decodeJSONBody(t, rr, &response)
		if response.Error != "start and end parameters are required" {
			t.Fatalf("unexpected error response: %#v", response)
		}
	})

	t.Run("invalid bucket falls back to default", func(t *testing.T) {
		storage := &dashboardStorageStub{
			getHourlyStatsFn: func(startTime, endTime string, bucketMinutes int, orgFilter string) (*model.HourlyStatsResponse, error) {
				if startTime != "2026-03-01T00:00:00Z" || endTime != "2026-03-01T12:00:00Z" {
					t.Fatalf("unexpected time range %q - %q", startTime, endTime)
				}
				if bucketMinutes != defaultBucketMinutes {
					t.Fatalf("expected default bucket %d, got %d", defaultBucketMinutes, bucketMinutes)
				}
				if orgFilter != "org-2" {
					t.Fatalf("expected org filter org-2, got %q", orgFilter)
				}

				return &model.HourlyStatsResponse{
					HourlyStats: []model.HourlyTokens{{Hour: 9, Tokens: 123, Requests: 3}},
				}, nil
			},
		}
		req := httptest.NewRequest(http.MethodGet, "/api/dashboard/hourly?start=2026-03-01T00:00:00Z&end=2026-03-01T12:00:00Z&bucket=bad&org=org-2", nil)
		rr := httptest.NewRecorder()

		newTestHandler(storage).GetHourlyStats(rr, req)

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

		var response model.HourlyStatsResponse
		decodeJSONBody(t, rr, &response)
		if len(response.HourlyStats) != 1 || response.HourlyStats[0].Tokens != 123 {
			t.Fatalf("unexpected hourly stats payload: %#v", response)
		}
	})
}

func TestGetModelStatsRejectsMissingRange(t *testing.T) {
	req := httptest.NewRequest(http.MethodGet, "/api/dashboard/models?end=2026-03-01T12:00:00Z", nil)
	rr := httptest.NewRecorder()

	newTestHandler(&dashboardStorageStub{}).GetModelStats(rr, req)

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

	var response model.ErrorResponse
	decodeJSONBody(t, rr, &response)
	if response.Error != "start and end parameters are required" {
		t.Fatalf("unexpected error response: %#v", response)
	}
}

func TestGetModelStatsPassesFiltersThrough(t *testing.T) {
	storage := &dashboardStorageStub{
		getModelStatsFn: func(startTime, endTime, orgFilter string) (*model.ModelStatsResponse, error) {
			if startTime != "2026-03-01T00:00:00Z" || endTime != "2026-03-01T12:00:00Z" {
				t.Fatalf("unexpected range %q - %q", startTime, endTime)
			}
			if orgFilter != "org-models" {
				t.Fatalf("expected org filter org-models, got %q", orgFilter)
			}
			return &model.ModelStatsResponse{
				ModelStats: []model.ModelTokens{{Model: "claude-opus-4-6", Tokens: 321, Requests: 4}},
			}, nil
		},
	}
	req := httptest.NewRequest(http.MethodGet, "/api/dashboard/models?start=2026-03-01T00:00:00Z&end=2026-03-01T12:00:00Z&org=org-models", nil)
	rr := httptest.NewRecorder()

	newTestHandler(storage).GetModelStats(rr, req)

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

	var response model.ModelStatsResponse
	decodeJSONBody(t, rr, &response)
	if len(response.ModelStats) != 1 || response.ModelStats[0].Model != "claude-opus-4-6" {
		t.Fatalf("unexpected model stats payload: %#v", response)
	}
}

func TestGetOrganizationsReturnsEmptySliceWhenStorageReturnsNil(t *testing.T) {
	storage := &dashboardStorageStub{
		getDistinctOrganizationsFn: func() ([]string, error) {
			return nil, nil
		},
	}
	req := httptest.NewRequest(http.MethodGet, "/api/organizations", nil)
	rr := httptest.NewRecorder()

	newTestHandler(storage).GetOrganizations(rr, req)

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

	var response struct {
		Organizations []string `json:"organizations"`
	}
	decodeJSONBody(t, rr, &response)
	if len(response.Organizations) != 0 {
		t.Fatalf("expected empty organizations list, got %#v", response.Organizations)
	}
}

func TestGetLatestRequestDateReturnsNullWhenStorageHasNoData(t *testing.T) {
	storage := &dashboardStorageStub{
		getLatestRequestDateFn: func() (*time.Time, error) {
			return nil, nil
		},
	}
	req := httptest.NewRequest(http.MethodGet, "/api/latest-request-date", nil)
	rr := httptest.NewRecorder()

	newTestHandler(storage).GetLatestRequestDate(rr, req)

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

	var response struct {
		LatestDate *time.Time `json:"latestDate"`
	}
	decodeJSONBody(t, rr, &response)
	if response.LatestDate != nil {
		t.Fatalf("expected latestDate to be null, got %#v", response.LatestDate)
	}
}
