package litigationplanner

import (
	"context"
	"testing"

	"github.com/google/uuid"
)

// Tests for t-paliad-342 / youpcorg#2568 + #2570.
//
// Two paired engine semantics:
//
//   - Optional rules (priority='optional') don't auto-fire in the
//     default timeline; the caller opts in via
//     CalcOptions.IncludeOptional.
//   - Rules with explicit trigger_event_id anchor on the trigger
//     event's date (CalcOptions.TriggerEventAnchors keyed by
//     trigger_events.code). Missing anchor = render conditional
//     instead of fabricating a date off the proceeding's trigger date.

// stubCatalogWithTriggers extends stubCatalog with a trigger-events
// map so the engine can resolve TriggerEventID → code for the
// trigger-anchor branch. stubCatalog from before_court_set_anchor_test.go
// returns an empty map, which suffices for tests that don't exercise
// trigger_event_id; here we need real entries.
type stubCatalogWithTriggers struct {
	stubCatalog
	triggerEvents map[int64]TriggerEvent
}

func (s *stubCatalogWithTriggers) LoadTriggerEventsByIDs(_ context.Context, ids []int64) (map[int64]TriggerEvent, error) {
	out := make(map[int64]TriggerEvent, len(ids))
	for _, id := range ids {
		if te, ok := s.triggerEvents[id]; ok {
			out[id] = te
		}
	}
	return out, nil
}

// mandatory_socRule builds a minimal SoC root rule + the proceeding
// type wrapper that nearly every test below needs.
func mandatorySocFixture(t *testing.T) (ProceedingType, Rule, uuid.UUID) {
	t.Helper()
	jurisdiction := "UPC"
	procID := 1
	pt := ProceedingType{
		ID:           procID,
		Code:         "upc.inf.cfi",
		Name:         "Verletzungsverfahren",
		Jurisdiction: &jurisdiction,
		IsActive:     true,
	}
	socID, _ := uuid.NewRandom()
	socCode := "upc.inf.cfi.soc"
	procIDPtr := &procID
	str := func(s string) *string { return &s }
	soc := Rule{
		ID:               socID,
		ProceedingTypeID: procIDPtr,
		ParentID:         nil,
		SubmissionCode:   &socCode,
		Name:             "Klageerhebung",
		NameEN:           "SoC",
		PrimaryParty:     str("claimant"),
		DurationValue:    0,
		DurationUnit:     "months",
		Timing:           str("after"),
		SequenceOrder:    0,
		IsActive:         true,
		LifecycleState:   "published",
		Priority:         "mandatory",
	}
	return pt, soc, socID
}

// TestCalculate_TriggerEventIDWithoutAnchor_Suppressed verifies the
// RoP.109.5 bug from youpcorg#2568: a rule with trigger_event_id and
// no parent_id must NOT fall back to the proceeding's trigger date.
// The buggy behaviour rendered the rule with a fabricated date 2 weeks
// before the user's SoC date.
func TestCalculate_TriggerEventIDWithoutAnchor_Suppressed(t *testing.T) {
	ctx := context.Background()
	pt, soc, _ := mandatorySocFixture(t)

	str := func(s string) *string { return &s }
	procIDPtr := &pt.ID
	ruleID, _ := uuid.NewRandom()
	ruleCode := "upc.inf.cfi.rop_109_5"
	rop109_5Trigger := int64(49)
	rop109_5 := Rule{
		ID:               ruleID,
		ProceedingTypeID: procIDPtr,
		ParentID:         nil,
		SubmissionCode:   &ruleCode,
		Name:             "Vorbereitung mündliche Verhandlung",
		NameEN:           "Oral hearing preparation",
		PrimaryParty:     str("both"),
		DurationValue:    2,
		DurationUnit:     "weeks",
		Timing:           str("before"),
		SequenceOrder:    100,
		IsActive:         true,
		LifecycleState:   "published",
		Priority:         "mandatory",
		TriggerEventID:   &rop109_5Trigger,
	}

	cat := &stubCatalogWithTriggers{
		stubCatalog: stubCatalog{pt: pt, rules: []Rule{soc, rop109_5}},
		triggerEvents: map[int64]TriggerEvent{
			49: {ID: 49, Code: "oral_hearing", Name: "Oral hearing", NameDE: "Mündliche Verhandlung"},
		},
	}

	timeline, err := Calculate(ctx, "upc.inf.cfi", "2026-05-26", CalcOptions{}, cat, noOpHolidays{}, fixedCourts{})
	if err != nil {
		t.Fatalf("Calculate: %v", err)
	}

	byCode := make(map[string]TimelineEntry, len(timeline.Deadlines))
	for _, d := range timeline.Deadlines {
		byCode[d.Code] = d
	}

	rop, ok := byCode[ruleCode]
	if !ok {
		t.Fatalf("RoP.109.5 missing from timeline; want present with conditional-no-date")
	}
	if rop.DueDate != "" {
		t.Errorf("RoP.109.5: DueDate=%q, want empty (no oral_hearing anchor supplied)", rop.DueDate)
	}
	if !rop.IsConditional {
		t.Errorf("RoP.109.5: IsConditional=%v, want true", rop.IsConditional)
	}
	if timeline.RulesAwaitingAnchor != 1 {
		t.Errorf("RulesAwaitingAnchor=%d, want 1", timeline.RulesAwaitingAnchor)
	}
}

// TestCalculate_TriggerEventIDWithAnchor_Renders verifies the
// caller-supplied trigger-event anchor produces correct arithmetic.
// 2 weeks before 2026-10-15 = 2026-10-01.
func TestCalculate_TriggerEventIDWithAnchor_Renders(t *testing.T) {
	ctx := context.Background()
	pt, soc, _ := mandatorySocFixture(t)

	str := func(s string) *string { return &s }
	procIDPtr := &pt.ID
	ruleID, _ := uuid.NewRandom()
	ruleCode := "upc.inf.cfi.rop_109_5"
	rop109_5Trigger := int64(49)
	rop109_5 := Rule{
		ID:               ruleID,
		ProceedingTypeID: procIDPtr,
		ParentID:         nil,
		SubmissionCode:   &ruleCode,
		Name:             "Vorbereitung mündliche Verhandlung",
		NameEN:           "Oral hearing preparation",
		PrimaryParty:     str("both"),
		DurationValue:    2,
		DurationUnit:     "weeks",
		Timing:           str("before"),
		SequenceOrder:    100,
		IsActive:         true,
		LifecycleState:   "published",
		Priority:         "mandatory",
		TriggerEventID:   &rop109_5Trigger,
	}

	cat := &stubCatalogWithTriggers{
		stubCatalog: stubCatalog{pt: pt, rules: []Rule{soc, rop109_5}},
		triggerEvents: map[int64]TriggerEvent{
			49: {ID: 49, Code: "oral_hearing", Name: "Oral hearing", NameDE: "Mündliche Verhandlung"},
		},
	}

	opts := CalcOptions{
		TriggerEventAnchors: map[string]string{
			"oral_hearing": "2026-10-15",
		},
	}
	timeline, err := Calculate(ctx, "upc.inf.cfi", "2026-05-26", opts, cat, noOpHolidays{}, fixedCourts{})
	if err != nil {
		t.Fatalf("Calculate: %v", err)
	}

	byCode := make(map[string]TimelineEntry, len(timeline.Deadlines))
	for _, d := range timeline.Deadlines {
		byCode[d.Code] = d
	}

	rop := byCode[ruleCode]
	if rop.DueDate != "2026-10-01" {
		t.Errorf("RoP.109.5: DueDate=%q, want 2026-10-01 (2 weeks before oral_hearing 2026-10-15)", rop.DueDate)
	}
	if rop.IsConditional {
		t.Errorf("RoP.109.5: IsConditional=%v, want false (anchor supplied)", rop.IsConditional)
	}
	if timeline.RulesAwaitingAnchor != 0 {
		t.Errorf("RulesAwaitingAnchor=%d, want 0", timeline.RulesAwaitingAnchor)
	}
}

// TestCalculate_MandatoryRule_RendersByDefault is the control case for
// the optional-suppression fix: mandatory rules render with their
// computed dates by default. Prevents regression where the optional
// filter accidentally drops mandatory rules too.
func TestCalculate_MandatoryRule_RendersByDefault(t *testing.T) {
	ctx := context.Background()
	pt, soc, socID := mandatorySocFixture(t)

	str := func(s string) *string { return &s }
	procIDPtr := &pt.ID
	replyID, _ := uuid.NewRandom()
	replyCode := "upc.inf.cfi.reply"
	reply := Rule{
		ID:               replyID,
		ProceedingTypeID: procIDPtr,
		ParentID:         &socID,
		SubmissionCode:   &replyCode,
		Name:             "Klageerwiderung",
		NameEN:           "Reply to SoC",
		PrimaryParty:     str("defendant"),
		DurationValue:    3,
		DurationUnit:     "months",
		Timing:           str("after"),
		SequenceOrder:    10,
		IsActive:         true,
		LifecycleState:   "published",
		Priority:         "mandatory",
	}

	cat := &stubCatalogWithTriggers{
		stubCatalog: stubCatalog{pt: pt, rules: []Rule{soc, reply}},
	}

	timeline, err := Calculate(ctx, "upc.inf.cfi", "2026-05-26", CalcOptions{}, cat, noOpHolidays{}, fixedCourts{})
	if err != nil {
		t.Fatalf("Calculate: %v", err)
	}

	byCode := make(map[string]TimelineEntry, len(timeline.Deadlines))
	for _, d := range timeline.Deadlines {
		byCode[d.Code] = d
	}

	got, ok := byCode[replyCode]
	if !ok {
		t.Fatalf("mandatory reply rule missing from default timeline")
	}
	if got.DueDate != "2026-08-26" {
		t.Errorf("reply: DueDate=%q, want 2026-08-26 (3 months after SoC)", got.DueDate)
	}
}

// TestCalculate_OptionalRule_SuppressedByDefault pins the
// youpcorg#2570 fix: priority='optional' rules don't render in the
// default timeline. The caller opts in via IncludeOptional=true.
func TestCalculate_OptionalRule_SuppressedByDefault(t *testing.T) {
	ctx := context.Background()
	pt, soc, socID := mandatorySocFixture(t)

	str := func(s string) *string { return &s }
	procIDPtr := &pt.ID
	confID, _ := uuid.NewRandom()
	confCode := "upc.inf.cfi.rop_262_2"
	conf := Rule{
		ID:               confID,
		ProceedingTypeID: procIDPtr,
		ParentID:         &socID,
		SubmissionCode:   &confCode,
		Name:             "Erwiderung Vertraulichkeitsantrag",
		NameEN:           "Reply to confidentiality motion",
		PrimaryParty:     str("both"),
		DurationValue:    14,
		DurationUnit:     "days",
		Timing:           str("after"),
		SequenceOrder:    20,
		IsActive:         true,
		LifecycleState:   "published",
		Priority:         "optional",
	}

	cat := &stubCatalogWithTriggers{
		stubCatalog: stubCatalog{pt: pt, rules: []Rule{soc, conf}},
	}

	timeline, err := Calculate(ctx, "upc.inf.cfi", "2026-05-26", CalcOptions{}, cat, noOpHolidays{}, fixedCourts{})
	if err != nil {
		t.Fatalf("Calculate: %v", err)
	}

	for _, d := range timeline.Deadlines {
		if d.Code == confCode {
			t.Errorf("optional R.262(2) rule rendered in default timeline (DueDate=%q); want suppressed", d.DueDate)
		}
	}
}

// TestCalculate_OptionalRule_RendersWithIncludeOptional verifies the
// opt-in path: when the caller passes IncludeOptional=true, optional
// rules show up in the timeline with their computed dates.
func TestCalculate_OptionalRule_RendersWithIncludeOptional(t *testing.T) {
	ctx := context.Background()
	pt, soc, socID := mandatorySocFixture(t)

	str := func(s string) *string { return &s }
	procIDPtr := &pt.ID
	confID, _ := uuid.NewRandom()
	confCode := "upc.inf.cfi.rop_262_2"
	conf := Rule{
		ID:               confID,
		ProceedingTypeID: procIDPtr,
		ParentID:         &socID,
		SubmissionCode:   &confCode,
		Name:             "Erwiderung Vertraulichkeitsantrag",
		NameEN:           "Reply to confidentiality motion",
		PrimaryParty:     str("both"),
		DurationValue:    14,
		DurationUnit:     "days",
		Timing:           str("after"),
		SequenceOrder:    20,
		IsActive:         true,
		LifecycleState:   "published",
		Priority:         "optional",
	}

	cat := &stubCatalogWithTriggers{
		stubCatalog: stubCatalog{pt: pt, rules: []Rule{soc, conf}},
	}

	timeline, err := Calculate(ctx, "upc.inf.cfi", "2026-05-26", CalcOptions{IncludeOptional: true}, cat, noOpHolidays{}, fixedCourts{})
	if err != nil {
		t.Fatalf("Calculate: %v", err)
	}

	byCode := make(map[string]TimelineEntry, len(timeline.Deadlines))
	for _, d := range timeline.Deadlines {
		byCode[d.Code] = d
	}

	got, ok := byCode[confCode]
	if !ok {
		t.Fatalf("optional R.262(2) missing from timeline despite IncludeOptional=true")
	}
	// R.262(2) is the "optional opposing-side" pattern (priority=optional,
	// primary_party=both, parent=SoC root) — the engine renders this as
	// IsConditional (no concrete date) per the t-paliad-289 logic
	// preserved in the walk. The point of this test is that the rule
	// is no longer suppressed wholesale by the t-paliad-342 default —
	// it surfaces, just with the conditional-render UX.
	if !got.IsConditional {
		t.Errorf("R.262(2): IsConditional=%v, want true (optional opposing-side, no real trigger recorded)", got.IsConditional)
	}
}

// TestCalculate_TriggerEventID_MalformedAnchor_Errors ensures
// malformed dates in TriggerEventAnchors fail fast at the top of the
// engine, before any rule walking — same protocol as AnchorOverrides.
func TestCalculate_TriggerEventID_MalformedAnchor_Errors(t *testing.T) {
	ctx := context.Background()
	pt, soc, _ := mandatorySocFixture(t)

	cat := &stubCatalogWithTriggers{
		stubCatalog: stubCatalog{pt: pt, rules: []Rule{soc}},
	}

	opts := CalcOptions{
		TriggerEventAnchors: map[string]string{
			"oral_hearing": "15-10-2026", // wrong format
		},
	}
	_, err := Calculate(ctx, "upc.inf.cfi", "2026-05-26", opts, cat, noOpHolidays{}, fixedCourts{})
	if err == nil {
		t.Fatalf("Calculate: want error on malformed TriggerEventAnchors date, got nil")
	}
}
