package services

import (
	"encoding/json"
	"testing"
)

// t-paliad-283 regression: the bar's chip clicks POST a `predicates`
// payload shaped as `{<source>: <per-source>}`. The Go side previously
// declared `Predicates map[DataSource]Predicates` — a doubled-nested
// shape — which silently unmarshalled the bar's payload as no-op
// narrowing. This test pins the wire shape so the contract can't drift
// again.
//
// Run with `go test ./internal/services/`.

func TestFilterSpec_FlatPredicatesWireShape(t *testing.T) {
	// The shape every chip click in the FilterBar emits: predicates is
	// keyed by data source, value is the per-source predicate struct
	// directly. Doubled-nesting would unmarshal as empty Predicates.
	const wire = `{
		"version": 1,
		"sources": ["deadline", "appointment", "project_event", "approval_request"],
		"scope": {"projects": {"mode": "all_visible"}},
		"time": {"field": "auto", "horizon": "past_30d"},
		"predicates": {
			"deadline": {"status": ["pending"]},
			"appointment": {"appointment_types": ["hearing"]},
			"project_event": {"event_types": ["deadline_created"]},
			"approval_request": {"viewer_role": "any_visible", "status": ["pending"]}
		}
	}`

	var spec FilterSpec
	if err := json.Unmarshal([]byte(wire), &spec); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
	if err := spec.Validate(); err != nil {
		t.Fatalf("validate: %v", err)
	}
	if spec.Predicates == nil {
		t.Fatal("predicates must be non-nil after unmarshalling the bar's shape")
	}
	if spec.Predicates.Deadline == nil || len(spec.Predicates.Deadline.Status) != 1 || spec.Predicates.Deadline.Status[0] != "pending" {
		t.Errorf("deadline.status must round-trip, got %+v", spec.Predicates.Deadline)
	}
	if spec.Predicates.Appointment == nil || len(spec.Predicates.Appointment.AppointmentTypes) != 1 {
		t.Errorf("appointment.appointment_types must round-trip, got %+v", spec.Predicates.Appointment)
	}
	if spec.Predicates.ProjectEvent == nil || len(spec.Predicates.ProjectEvent.EventTypes) != 1 {
		t.Errorf("project_event.event_types must round-trip, got %+v", spec.Predicates.ProjectEvent)
	}
	if spec.Predicates.ApprovalRequest == nil || spec.Predicates.ApprovalRequest.ViewerRole != "any_visible" {
		t.Errorf("approval_request.viewer_role must round-trip, got %+v", spec.Predicates.ApprovalRequest)
	}
}

// The shipped FilterSpec must marshal back to exactly the flat shape
// the frontend declares in views/types.ts. Otherwise /api/views/system
// (which serializes the InboxSystemView's Filter for the bar) returns a
// shape the frontend can't consume without translation gymnastics.
func TestFilterSpec_MarshalFlatPredicatesShape(t *testing.T) {
	spec := FilterSpec{
		Version: SpecVersion,
		Sources: []DataSource{SourceDeadline},
		Scope:   ScopeSpec{Projects: ScopeProjects{Mode: ScopeAllVisible}},
		Time:    TimeSpec{Horizon: HorizonNext30d, Field: FieldAuto},
		Predicates: &Predicates{
			Deadline: &DeadlinePredicates{Status: []string{"pending"}},
		},
	}
	b, err := json.Marshal(spec)
	if err != nil {
		t.Fatalf("marshal: %v", err)
	}
	// Parse back generically so the assertion is on the wire shape, not
	// on the Go type system that produced it.
	var raw map[string]json.RawMessage
	if err := json.Unmarshal(b, &raw); err != nil {
		t.Fatalf("re-unmarshal: %v", err)
	}
	var preds map[string]json.RawMessage
	if err := json.Unmarshal(raw["predicates"], &preds); err != nil {
		t.Fatalf("predicates re-unmarshal: %v", err)
	}
	dl, ok := preds["deadline"]
	if !ok {
		t.Fatal("predicates.deadline missing — wire shape regressed")
	}
	var dlBody map[string]json.RawMessage
	if err := json.Unmarshal(dl, &dlBody); err != nil {
		t.Fatalf("deadline body unmarshal: %v", err)
	}
	if _, ok := dlBody["status"]; !ok {
		t.Errorf("predicates.deadline.status must be a top-level field; doubled-nesting reappeared. Body: %s", string(dl))
	}
	if _, ok := dlBody["deadline"]; ok {
		t.Errorf("predicates.deadline must NOT wrap a nested deadline key — that's the t-paliad-283 bug. Body: %s", string(dl))
	}
}

// End-to-end pin: the bar's payload after the user clicks
// "Frist-Status: Erledigt" (completed) must produce a spec whose
// runDeadlines branch narrows to completed deadlines. Without the
// t-paliad-283 fix, the unmarshal silently produced an empty Predicates
// and the SQL ran without the `status='completed'` clause.
func TestFilterSpec_BarChipPayloadNarrowsDeadlineStatus(t *testing.T) {
	const barPayload = `{
		"version": 1,
		"sources": ["deadline"],
		"scope": {"projects": {"mode": "all_visible"}},
		"time": {"field": "auto", "horizon": "past_30d"},
		"predicates": {"deadline": {"status": ["completed"]}}
	}`
	var spec FilterSpec
	if err := json.Unmarshal([]byte(barPayload), &spec); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
	if spec.Predicates == nil || spec.Predicates.Deadline == nil {
		t.Fatal("deadline predicate must survive the round-trip")
	}
	if len(spec.Predicates.Deadline.Status) != 1 || spec.Predicates.Deadline.Status[0] != "completed" {
		t.Errorf("deadline.status must be [\"completed\"], got %+v", spec.Predicates.Deadline.Status)
	}
}
