package handlers

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

	"mgit.msbls.de/m/paliad/internal/services"
)

// Pins t-paliad-160 §B: mapApprovalError must surface ErrConcurrentPending
// as a 409 with code=awaiting_approval, and PendingApprovalError must
// additionally carry the request_id + required_role so the UI can offer a
// withdraw button.
func TestMapApprovalError_ConcurrentPending409(t *testing.T) {
	w := httptest.NewRecorder()
	if !mapApprovalError(w, services.ErrConcurrentPending) {
		t.Fatal("mapApprovalError returned false for ErrConcurrentPending")
	}
	if w.Code != http.StatusConflict {
		t.Fatalf("status = %d, want 409", w.Code)
	}
	var body map[string]string
	if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
		t.Fatalf("decode body: %v", err)
	}
	if body["code"] != "awaiting_approval" {
		t.Errorf("code = %q, want awaiting_approval", body["code"])
	}
	if _, ok := body["request_id"]; ok {
		t.Errorf("bare ErrConcurrentPending should not carry request_id, got %q", body["request_id"])
	}
}

func TestMapApprovalError_PendingApprovalErrorCarriesRequestID(t *testing.T) {
	w := httptest.NewRecorder()
	pe := services.NewPendingApprovalError("11111111-2222-3333-4444-555555555555", "associate")
	if !mapApprovalError(w, pe) {
		t.Fatal("mapApprovalError returned false for PendingApprovalError")
	}
	if w.Code != http.StatusConflict {
		t.Fatalf("status = %d, want 409", w.Code)
	}
	var body map[string]string
	if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
		t.Fatalf("decode body: %v", err)
	}
	if body["code"] != "awaiting_approval" {
		t.Errorf("code = %q, want awaiting_approval", body["code"])
	}
	if body["request_id"] != "11111111-2222-3333-4444-555555555555" {
		t.Errorf("request_id = %q, want the wrapped uuid", body["request_id"])
	}
	if body["required_role"] != "associate" {
		t.Errorf("required_role = %q, want associate", body["required_role"])
	}
}

func TestMapApprovalError_NoQualifiedApprover409(t *testing.T) {
	w := httptest.NewRecorder()
	if !mapApprovalError(w, services.ErrNoQualifiedApprover) {
		t.Fatal("mapApprovalError returned false for ErrNoQualifiedApprover")
	}
	if w.Code != http.StatusConflict {
		t.Fatalf("status = %d, want 409", w.Code)
	}
	var body map[string]string
	_ = json.Unmarshal(w.Body.Bytes(), &body)
	if body["code"] != "no_qualified_approver" {
		t.Errorf("code = %q, want no_qualified_approver", body["code"])
	}
}

func TestMapApprovalError_MissReturnsFalse(t *testing.T) {
	w := httptest.NewRecorder()
	if mapApprovalError(w, services.ErrInvalidInput) {
		t.Error("mapApprovalError matched ErrInvalidInput; that's writeServiceError's job")
	}
	if w.Code != http.StatusOK {
		t.Errorf("status = %d, want 200 (recorder default — nothing written)", w.Code)
	}
}

// TestMapApprovalError_SuggestionRequiresChange400 pins t-paliad-216:
// a no-op suggest-changes (no counter diff + no note) surfaces as a 400
// with code suggestion_requires_change so the frontend can disable the
// submit button instead of letting the user click into a dead-end alert.
func TestMapApprovalError_SuggestionRequiresChange400(t *testing.T) {
	w := httptest.NewRecorder()
	if !mapApprovalError(w, services.ErrSuggestionRequiresChange) {
		t.Fatal("mapApprovalError returned false for ErrSuggestionRequiresChange")
	}
	if w.Code != http.StatusBadRequest {
		t.Fatalf("status = %d, want 400", w.Code)
	}
	var body map[string]string
	_ = json.Unmarshal(w.Body.Bytes(), &body)
	if body["code"] != "suggestion_requires_change" {
		t.Errorf("code = %q, want suggestion_requires_change", body["code"])
	}
}

// TestMapApprovalError_SuggestionLifecycleInvalid400 pins t-paliad-216:
// suggest-changes on a create/delete lifecycle is rejected with a clean
// 400 + code suggestion_lifecycle_invalid so the frontend can hide the
// button for those rows.
func TestMapApprovalError_SuggestionLifecycleInvalid400(t *testing.T) {
	w := httptest.NewRecorder()
	if !mapApprovalError(w, services.ErrSuggestionLifecycleInvalid) {
		t.Fatal("mapApprovalError returned false for ErrSuggestionLifecycleInvalid")
	}
	if w.Code != http.StatusBadRequest {
		t.Fatalf("status = %d, want 400", w.Code)
	}
	var body map[string]string
	_ = json.Unmarshal(w.Body.Bytes(), &body)
	if body["code"] != "suggestion_lifecycle_invalid" {
		t.Errorf("code = %q, want suggestion_lifecycle_invalid", body["code"])
	}
}

// TestParseInboxFilter_DropsUnknownStatus pins t-paliad-160 §D regression
// hardening: a stray ?status=foo from a stale frontend build (or an
// attacker scoping us out of our own list) must NOT shadow rows out of
// the result set. The handler silently drops anything not on the allowlist.
func TestParseInboxFilter_DropsUnknownStatus(t *testing.T) {
	cases := []struct {
		raw   string
		want  string
	}{
		{"", ""},
		{"pending", "pending"},
		{"approved", "approved"},
		{"rejected", "rejected"},
		{"revoked", "revoked"},
		{"superseded", "superseded"},
		{"changes_requested", "changes_requested"}, // t-paliad-216
		{"foo", ""},                // unknown — dropped
		{"DROP+TABLE", ""},         // hostile — dropped
		{"PENDING", ""},            // case mismatch — dropped (we don't normalise)
	}
	for _, tc := range cases {
		t.Run(tc.raw, func(t *testing.T) {
			req := httptest.NewRequest(http.MethodGet, "/api/inbox/mine?status="+tc.raw, nil)
			f := parseInboxFilter(req)
			if f.Status != tc.want {
				t.Errorf("parseInboxFilter(%q).Status = %q, want %q", tc.raw, f.Status, tc.want)
			}
		})
	}
}
