package services

// Pure-Go tests for FilterSpec — no DB touch. Cover happy path, every
// reject branch, and the cross-field constraints (Q26 horizon-clamp,
// scope mode/IDs invariants).

import (
	"errors"
	"testing"
	"time"

	"github.com/google/uuid"
)

func validBaseSpec() FilterSpec {
	return FilterSpec{
		Version: SpecVersion,
		Sources: []DataSource{SourceDeadline},
		Scope:   ScopeSpec{Projects: ScopeProjects{Mode: ScopeAllVisible}},
		Time:    TimeSpec{Horizon: HorizonNext30d, Field: FieldAuto},
	}
}

func TestFilterSpec_HappyPath(t *testing.T) {
	s := validBaseSpec()
	if err := s.Validate(); err != nil {
		t.Fatalf("base spec should validate: %v", err)
	}
}

func TestFilterSpec_DefaultIsValid(t *testing.T) {
	s := DefaultFilterSpec()
	if err := s.Validate(); err != nil {
		t.Fatalf("DefaultFilterSpec must validate: %v", err)
	}
}

func TestFilterSpec_Version(t *testing.T) {
	s := validBaseSpec()
	s.Version = 2
	err := s.Validate()
	if !errors.Is(err, ErrInvalidInput) {
		t.Fatalf("unknown version must reject with ErrInvalidInput, got %v", err)
	}
}

func TestFilterSpec_NoSources(t *testing.T) {
	s := validBaseSpec()
	s.Sources = nil
	if err := s.Validate(); !errors.Is(err, ErrInvalidInput) {
		t.Fatalf("empty sources must reject, got %v", err)
	}
}

func TestFilterSpec_UnknownSource(t *testing.T) {
	s := validBaseSpec()
	s.Sources = []DataSource{"bogus"}
	if err := s.Validate(); !errors.Is(err, ErrInvalidInput) {
		t.Fatalf("unknown source must reject, got %v", err)
	}
}

func TestFilterSpec_DuplicateSource(t *testing.T) {
	s := validBaseSpec()
	s.Sources = []DataSource{SourceDeadline, SourceDeadline}
	if err := s.Validate(); !errors.Is(err, ErrInvalidInput) {
		t.Fatalf("duplicate source must reject, got %v", err)
	}
}

func TestFilterSpec_AllSourcesAccepted(t *testing.T) {
	s := validBaseSpec()
	s.Sources = []DataSource{SourceDeadline, SourceAppointment, SourceProjectEvent, SourceApprovalRequest}
	if err := s.Validate(); err != nil {
		t.Fatalf("all four sources together must validate: %v", err)
	}
}

func TestFilterSpec_ScopeAllVisibleRejectsIDs(t *testing.T) {
	s := validBaseSpec()
	s.Scope.Projects.IDs = []uuid.UUID{uuid.New()}
	if err := s.Validate(); !errors.Is(err, ErrInvalidInput) {
		t.Fatalf("ids on all_visible mode must reject, got %v", err)
	}
}

func TestFilterSpec_ScopeExplicitNeedsIDs(t *testing.T) {
	s := validBaseSpec()
	s.Scope.Projects = ScopeProjects{Mode: ScopeExplicit}
	if err := s.Validate(); !errors.Is(err, ErrInvalidInput) {
		t.Fatalf("explicit mode without ids must reject, got %v", err)
	}
}

func TestFilterSpec_ScopeExplicitWithIDsValidates(t *testing.T) {
	s := validBaseSpec()
	s.Scope.Projects = ScopeProjects{Mode: ScopeExplicit, IDs: []uuid.UUID{uuid.New()}}
	if err := s.Validate(); err != nil {
		t.Fatalf("explicit mode + ids must validate: %v", err)
	}
}

func TestFilterSpec_PersonalOnlyConflictsWithExplicit(t *testing.T) {
	s := validBaseSpec()
	s.Scope.Projects = ScopeProjects{Mode: ScopeExplicit, IDs: []uuid.UUID{uuid.New()}}
	s.Scope.PersonalOnly = true
	if err := s.Validate(); !errors.Is(err, ErrInvalidInput) {
		t.Fatalf("personal_only + explicit projects must reject, got %v", err)
	}
}

func TestFilterSpec_HorizonAllRejectsWithoutExplicit(t *testing.T) {
	// Q26 lock-in: horizon=all is rejected unless scope.projects.mode=explicit.
	s := validBaseSpec()
	s.Time.Horizon = HorizonAll
	if err := s.Validate(); !errors.Is(err, ErrInvalidInput) {
		t.Fatalf("horizon=all without explicit projects must reject, got %v", err)
	}
}

func TestFilterSpec_HorizonAllAcceptsWithExplicit(t *testing.T) {
	s := validBaseSpec()
	s.Scope.Projects = ScopeProjects{Mode: ScopeExplicit, IDs: []uuid.UUID{uuid.New()}}
	s.Time.Horizon = HorizonAll
	if err := s.Validate(); err != nil {
		t.Fatalf("horizon=all with explicit projects must validate: %v", err)
	}
}

func TestFilterSpec_HorizonCustomNeedsBothBounds(t *testing.T) {
	s := validBaseSpec()
	s.Time.Horizon = HorizonCustom
	if err := s.Validate(); !errors.Is(err, ErrInvalidInput) {
		t.Fatalf("custom horizon without bounds must reject, got %v", err)
	}
}

func TestFilterSpec_HorizonCustomRejectsInvertedRange(t *testing.T) {
	s := validBaseSpec()
	s.Time.Horizon = HorizonCustom
	earlier := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
	later := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
	// to before from
	s.Time.From = &later
	s.Time.To = &earlier
	if err := s.Validate(); !errors.Is(err, ErrInvalidInput) {
		t.Fatalf("inverted from/to must reject, got %v", err)
	}
}

func TestFilterSpec_HorizonCustomAcceptsValidRange(t *testing.T) {
	s := validBaseSpec()
	s.Time.Horizon = HorizonCustom
	from := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
	to := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
	s.Time.From = &from
	s.Time.To = &to
	if err := s.Validate(); err != nil {
		t.Fatalf("valid custom range must accept: %v", err)
	}
}

// t-paliad-248: the symmetric date-range picker adds six new horizons —
// 1d/14d/all on each side. They must round-trip through validate without
// requiring scope.explicit (unlike HorizonAll which is a bidirectional-
// unbounded substrate scan and stays gated to ScopeExplicit per Q26).
func TestFilterSpec_NewSymmetricHorizonsValidate(t *testing.T) {
	for _, h := range []TimeHorizon{
		HorizonNext1d, HorizonNext14d, HorizonNextAll,
		HorizonPast1d, HorizonPast14d, HorizonPastAll,
	} {
		s := validBaseSpec()
		s.Time.Horizon = h
		if err := s.Validate(); err != nil {
			t.Fatalf("horizon %q must validate against a default scope: %v", h, err)
		}
	}
}

func TestFilterSpec_PredicatesRequireSourceSelected(t *testing.T) {
	s := validBaseSpec()
	s.Sources = []DataSource{SourceDeadline}
	s.Predicates = &Predicates{
		Appointment: &AppointmentPredicates{AppointmentTypes: []string{"hearing"}},
	}
	if err := s.Validate(); !errors.Is(err, ErrInvalidInput) {
		t.Fatalf("predicates on unselected source must reject, got %v", err)
	}
}

func TestFilterSpec_DeadlineStatusEnum(t *testing.T) {
	s := validBaseSpec()
	s.Predicates = &Predicates{
		Deadline: &DeadlinePredicates{Status: []string{"weird"}},
	}
	if err := s.Validate(); !errors.Is(err, ErrInvalidInput) {
		t.Fatalf("unknown deadline.status must reject, got %v", err)
	}
}

func TestFilterSpec_AppointmentTypeEnum(t *testing.T) {
	s := validBaseSpec()
	s.Sources = append(s.Sources, SourceAppointment)
	s.Predicates = &Predicates{
		Appointment: &AppointmentPredicates{AppointmentTypes: []string{"bogus"}},
	}
	if err := s.Validate(); !errors.Is(err, ErrInvalidInput) {
		t.Fatalf("unknown appointment_type must reject, got %v", err)
	}
}

func TestFilterSpec_ProjectEventKindMustBeKnown(t *testing.T) {
	s := validBaseSpec()
	s.Sources = []DataSource{SourceProjectEvent}
	s.Predicates = &Predicates{
		ProjectEvent: &ProjectEventPredicates{EventTypes: []string{"unknown_kind"}},
	}
	if err := s.Validate(); !errors.Is(err, ErrInvalidInput) {
		t.Fatalf("unknown project_event kind must reject, got %v", err)
	}
}

func TestFilterSpec_ApprovalViewerRoleEnum(t *testing.T) {
	s := validBaseSpec()
	s.Sources = []DataSource{SourceApprovalRequest}
	s.Predicates = &Predicates{
		ApprovalRequest: &ApprovalRequestPredicates{ViewerRole: "everyone"},
	}
	if err := s.Validate(); !errors.Is(err, ErrInvalidInput) {
		t.Fatalf("unknown viewer_role must reject, got %v", err)
	}
}

func TestFilterSpec_ApprovalRequestStatusEnum(t *testing.T) {
	s := validBaseSpec()
	s.Sources = []DataSource{SourceApprovalRequest}
	s.Predicates = &Predicates{
		ApprovalRequest: &ApprovalRequestPredicates{Status: []string{"weird"}},
	}
	if err := s.Validate(); !errors.Is(err, ErrInvalidInput) {
		t.Fatalf("unknown approval_request.status must reject, got %v", err)
	}
}

func TestFilterSpec_RoundTripJSON(t *testing.T) {
	original := FilterSpec{
		Version: SpecVersion,
		Sources: []DataSource{SourceDeadline, SourceApprovalRequest},
		Scope: ScopeSpec{
			Projects:     ScopeProjects{Mode: ScopeAllVisible},
			PersonalOnly: false,
		},
		Time: TimeSpec{Horizon: HorizonNext30d, Field: FieldAuto},
		Predicates: &Predicates{
			Deadline: &DeadlinePredicates{
				Status:         []string{"pending"},
				ApprovalStatus: []string{"approved", "pending"},
			},
			ApprovalRequest: &ApprovalRequestPredicates{
				ViewerRole: "approver_eligible",
				Status:     []string{"pending"},
			},
		},
	}
	b, err := MarshalFilterSpec(original)
	if err != nil {
		t.Fatalf("marshal: %v", err)
	}
	parsed, err := UnmarshalFilterSpec(b)
	if err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
	if parsed.Version != original.Version {
		t.Errorf("version mismatch: %d vs %d", parsed.Version, original.Version)
	}
	if len(parsed.Sources) != len(original.Sources) {
		t.Errorf("sources mismatch: %v vs %v", parsed.Sources, original.Sources)
	}
}

