package router

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/http/httptest"
	"os"
	"testing"

	"github.com/labstack/echo/v4"

	"github.com/treytartt/honeydue-api/internal/apperrors"
	"github.com/treytartt/honeydue-api/internal/dto/responses"
	"github.com/treytartt/honeydue-api/internal/i18n"
	"github.com/treytartt/honeydue-api/internal/services"
)

func TestMain(m *testing.M) {
	// Initialize i18n so LocalizedMessage returns real translations
	_ = i18n.Init()
	os.Exit(m.Run())
}

// makeContext creates a fresh echo.Context and response recorder for testing.
func makeContext(method, path string) (echo.Context, *httptest.ResponseRecorder) {
	e := echo.New()
	req := httptest.NewRequest(method, path, nil)
	req.Header.Set("Accept-Language", "en")
	rec := httptest.NewRecorder()
	c := e.NewContext(req, rec)
	// Set up i18n localizer on context (same key the middleware uses)
	if i18n.Bundle != nil {
		loc := i18n.NewLocalizer("en")
		c.Set(i18n.LocalizerKey, loc)
	}
	return c, rec
}

// decodeError reads the JSON response into an ErrorResponse.
func decodeError(t *testing.T, rec *httptest.ResponseRecorder) responses.ErrorResponse {
	t.Helper()
	var resp responses.ErrorResponse
	if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
		t.Fatalf("failed to decode error response: %v\nbody: %s", err, rec.Body.String())
	}
	return resp
}

// --- AppError branch ---

func TestErrorHandler_AppError_NotFound(t *testing.T) {
	c, rec := makeContext(http.MethodGet, "/tasks/1")
	err := apperrors.NotFound("error.task_not_found")
	customHTTPErrorHandler(err, c)
	if rec.Code != http.StatusNotFound {
		t.Errorf("code = %d, want %d", rec.Code, http.StatusNotFound)
	}
	resp := decodeError(t, rec)
	if resp.Error == "" {
		t.Error("expected non-empty error message")
	}
}

func TestErrorHandler_AppError_Forbidden(t *testing.T) {
	c, rec := makeContext(http.MethodGet, "/res/1")
	err := apperrors.Forbidden("error.task_access_denied")
	customHTTPErrorHandler(err, c)
	if rec.Code != http.StatusForbidden {
		t.Errorf("code = %d, want %d", rec.Code, http.StatusForbidden)
	}
}

func TestErrorHandler_AppError_BadRequest(t *testing.T) {
	c, rec := makeContext(http.MethodPost, "/tasks")
	err := apperrors.BadRequest("error.task_already_cancelled")
	customHTTPErrorHandler(err, c)
	if rec.Code != http.StatusBadRequest {
		t.Errorf("code = %d, want %d", rec.Code, http.StatusBadRequest)
	}
}

func TestErrorHandler_AppError_WithWrappedErr(t *testing.T) {
	c, rec := makeContext(http.MethodGet, "/x")
	err := apperrors.Internal(fmt.Errorf("db conn failed"))
	customHTTPErrorHandler(err, c)
	if rec.Code != http.StatusInternalServerError {
		t.Errorf("code = %d, want %d", rec.Code, http.StatusInternalServerError)
	}
}

func TestErrorHandler_AppError_I18nFallback(t *testing.T) {
	c, rec := makeContext(http.MethodGet, "/x")
	err := apperrors.NotFound("nonexistent.i18n.key").WithMessage("fallback msg")
	customHTTPErrorHandler(err, c)
	if rec.Code != http.StatusNotFound {
		t.Errorf("code = %d, want %d", rec.Code, http.StatusNotFound)
	}
	resp := decodeError(t, rec)
	if resp.Error != "fallback msg" {
		t.Errorf("error = %q, want %q", resp.Error, "fallback msg")
	}
}

// --- Echo HTTPError branch ---

func TestErrorHandler_EchoHTTPError_404(t *testing.T) {
	c, rec := makeContext(http.MethodGet, "/nope")
	err := echo.NewHTTPError(http.StatusNotFound, "not found")
	customHTTPErrorHandler(err, c)
	if rec.Code != http.StatusNotFound {
		t.Errorf("code = %d, want %d", rec.Code, http.StatusNotFound)
	}
	resp := decodeError(t, rec)
	if resp.Error != "not found" {
		t.Errorf("error = %q, want %q", resp.Error, "not found")
	}
}

func TestErrorHandler_EchoHTTPError_405(t *testing.T) {
	c, rec := makeContext(http.MethodGet, "/x")
	err := echo.ErrMethodNotAllowed
	customHTTPErrorHandler(err, c)
	if rec.Code != http.StatusMethodNotAllowed {
		t.Errorf("code = %d, want %d", rec.Code, http.StatusMethodNotAllowed)
	}
}

// --- Sentinel error branch ---

func TestErrorHandler_ErrTaskNotFound(t *testing.T) {
	c, rec := makeContext(http.MethodGet, "/t")
	customHTTPErrorHandler(services.ErrTaskNotFound, c)
	if rec.Code != http.StatusNotFound {
		t.Errorf("code = %d, want %d", rec.Code, http.StatusNotFound)
	}
}

func TestErrorHandler_ErrCompletionNotFound(t *testing.T) {
	c, rec := makeContext(http.MethodGet, "/t")
	customHTTPErrorHandler(services.ErrCompletionNotFound, c)
	if rec.Code != http.StatusNotFound {
		t.Errorf("code = %d, want %d", rec.Code, http.StatusNotFound)
	}
}

func TestErrorHandler_ErrTaskAccessDenied(t *testing.T) {
	c, rec := makeContext(http.MethodGet, "/t")
	customHTTPErrorHandler(services.ErrTaskAccessDenied, c)
	if rec.Code != http.StatusForbidden {
		t.Errorf("code = %d, want %d", rec.Code, http.StatusForbidden)
	}
}

func TestErrorHandler_ErrTaskAlreadyCancelled(t *testing.T) {
	c, rec := makeContext(http.MethodPost, "/t")
	customHTTPErrorHandler(services.ErrTaskAlreadyCancelled, c)
	if rec.Code != http.StatusBadRequest {
		t.Errorf("code = %d, want %d", rec.Code, http.StatusBadRequest)
	}
}

func TestErrorHandler_ErrTaskAlreadyArchived(t *testing.T) {
	c, rec := makeContext(http.MethodPost, "/t")
	customHTTPErrorHandler(services.ErrTaskAlreadyArchived, c)
	if rec.Code != http.StatusBadRequest {
		t.Errorf("code = %d, want %d", rec.Code, http.StatusBadRequest)
	}
}

func TestErrorHandler_ErrResidenceNotFound(t *testing.T) {
	c, rec := makeContext(http.MethodGet, "/r")
	customHTTPErrorHandler(services.ErrResidenceNotFound, c)
	if rec.Code != http.StatusNotFound {
		t.Errorf("code = %d, want %d", rec.Code, http.StatusNotFound)
	}
}

func TestErrorHandler_ErrResidenceAccessDenied(t *testing.T) {
	c, rec := makeContext(http.MethodGet, "/r")
	customHTTPErrorHandler(services.ErrResidenceAccessDenied, c)
	if rec.Code != http.StatusForbidden {
		t.Errorf("code = %d, want %d", rec.Code, http.StatusForbidden)
	}
}

func TestErrorHandler_ErrNotResidenceOwner(t *testing.T) {
	c, rec := makeContext(http.MethodDelete, "/r")
	customHTTPErrorHandler(services.ErrNotResidenceOwner, c)
	if rec.Code != http.StatusForbidden {
		t.Errorf("code = %d, want %d", rec.Code, http.StatusForbidden)
	}
}

func TestErrorHandler_ErrPropertiesLimitReached(t *testing.T) {
	c, rec := makeContext(http.MethodPost, "/r")
	customHTTPErrorHandler(services.ErrPropertiesLimitReached, c)
	if rec.Code != http.StatusForbidden {
		t.Errorf("code = %d, want %d", rec.Code, http.StatusForbidden)
	}
}

func TestErrorHandler_ErrCannotRemoveOwner(t *testing.T) {
	c, rec := makeContext(http.MethodDelete, "/r")
	customHTTPErrorHandler(services.ErrCannotRemoveOwner, c)
	if rec.Code != http.StatusBadRequest {
		t.Errorf("code = %d, want %d", rec.Code, http.StatusBadRequest)
	}
}

func TestErrorHandler_ErrShareCodeExpired(t *testing.T) {
	c, rec := makeContext(http.MethodPost, "/r")
	customHTTPErrorHandler(services.ErrShareCodeExpired, c)
	if rec.Code != http.StatusBadRequest {
		t.Errorf("code = %d, want %d", rec.Code, http.StatusBadRequest)
	}
}

func TestErrorHandler_ErrShareCodeInvalid(t *testing.T) {
	c, rec := makeContext(http.MethodPost, "/r")
	customHTTPErrorHandler(services.ErrShareCodeInvalid, c)
	if rec.Code != http.StatusNotFound {
		t.Errorf("code = %d, want %d", rec.Code, http.StatusNotFound)
	}
}

func TestErrorHandler_ErrUserAlreadyMember(t *testing.T) {
	c, rec := makeContext(http.MethodPost, "/r")
	customHTTPErrorHandler(services.ErrUserAlreadyMember, c)
	if rec.Code != http.StatusConflict {
		t.Errorf("code = %d, want %d", rec.Code, http.StatusConflict)
	}
}

// --- Default branch ---

func TestErrorHandler_UnknownError(t *testing.T) {
	c, rec := makeContext(http.MethodGet, "/x")
	customHTTPErrorHandler(errors.New("random unexpected error"), c)
	if rec.Code != http.StatusInternalServerError {
		t.Errorf("code = %d, want %d", rec.Code, http.StatusInternalServerError)
	}
}

// --- Committed response ---

func TestErrorHandler_CommittedResponse_Noop(t *testing.T) {
	c, rec := makeContext(http.MethodGet, "/x")
	// Write something to commit the response
	c.JSON(http.StatusOK, map[string]string{"ok": "true"})
	// Capture the body after first write
	bodyBefore := rec.Body.String()

	// Now call error handler — it should be a no-op
	customHTTPErrorHandler(errors.New("should be ignored"), c)
	bodyAfter := rec.Body.String()

	if bodyBefore != bodyAfter {
		t.Errorf("body changed after committed response:\nbefore: %s\nafter: %s", bodyBefore, bodyAfter)
	}
}
