package litigationplanner

import (
	"context"
	"errors"
	"testing"
	"time"

	"github.com/google/uuid"
)

// Regression test for t-paliad-304 / m/paliad#135.
//
// Reproduces the R.109.1 / R.109.4 anchor bug on upc.inf.cfi:
//   - Trigger (Klageerhebung):   parent_id=nil, duration=0,  !IsCourtSet, sequence_order=0
//   - Translation request:       parent_id=oral, duration=1mo before, sequence_order=45
//   - Interpreter cost:          parent_id=oral, duration=2w before, sequence_order=46
//   - Oral hearing:              parent_id=nil, duration=0,  IsCourtSet, sequence_order=50
//
// The "before" children are listed BEFORE the oral hearing in sequence
// order (because chronologically they happen before it). The engine walks
// rules in sequence_order, so when it processes the translation/
// interpreter rows, the oral hearing has not yet been processed →
// courtSet[oral.ID] is not yet set → parentIsCourtSet is false → the
// engine falls back to the trigger date as the base. Result: the timing=
// 'before' arithmetic produces 27.04.2026 (1mo before SoC) instead of
// the conditional-no-date treatment that a court-set parent should
// trigger.
//
// Expected post-fix: translation_request + interpreter_cost render as
// IsConditional (no concrete date) because their parent's date is
// court-set and the proceeding does not yet have an explicit override.

// stubCatalog implements lp.Catalog backed by an in-memory rule slice.
// Only LoadProceeding is needed for the engine path under test; the
// other interface methods return errors so an unintended call surfaces
// immediately.
type stubCatalog struct {
	pt    ProceedingType
	rules []Rule
}

func (s *stubCatalog) LoadProceeding(_ context.Context, code string, _ ProjectHint) (*ProceedingType, []Rule, error) {
	if code != s.pt.Code {
		return nil, nil, ErrUnknownProceedingType
	}
	rules := make([]Rule, len(s.rules))
	copy(rules, s.rules)
	pt := s.pt
	return &pt, rules, nil
}
func (s *stubCatalog) LoadProceedingByID(_ context.Context, _ int) (*ProceedingType, error) {
	return nil, errors.New("stubCatalog.LoadProceedingByID: not implemented")
}
func (s *stubCatalog) LoadRuleByID(_ context.Context, _ string) (*Rule, error) {
	return nil, errors.New("stubCatalog.LoadRuleByID: not implemented")
}
func (s *stubCatalog) LoadRuleByCode(_ context.Context, _, _ string) (*Rule, *ProceedingType, error) {
	return nil, nil, errors.New("stubCatalog.LoadRuleByCode: not implemented")
}
func (s *stubCatalog) LoadRulesByTriggerEvent(_ context.Context, _ int64) ([]Rule, error) {
	return nil, nil
}
func (s *stubCatalog) LoadTriggerEventsByIDs(_ context.Context, _ []int64) (map[int64]TriggerEvent, error) {
	return map[int64]TriggerEvent{}, nil
}
func (s *stubCatalog) LookupEvents(_ context.Context, _ EventLookupAxes, _ EventLookupDepth) ([]EventMatch, error) {
	return nil, nil
}
func (s *stubCatalog) LoadScenarios(_ context.Context, _ ScenarioFilter) ([]Scenario, error) {
	return nil, nil
}
func (s *stubCatalog) MatchScenario(_ context.Context, _ uuid.UUID) (*Scenario, error) {
	return nil, ErrUnknownScenario
}

// noOpHolidays never adjusts dates — the test fixture doesn't care about
// weekends or holidays, only about which base date the engine resolves.
type noOpHolidays struct{}

func (noOpHolidays) IsNonWorkingDay(_ time.Time, _, _ string) bool { return false }
func (noOpHolidays) AdjustForNonWorkingDays(d time.Time, _, _ string) (time.Time, time.Time, bool) {
	return d, d, false
}
func (noOpHolidays) AdjustForNonWorkingDaysBackward(d time.Time, _, _ string) (time.Time, time.Time, bool) {
	return d, d, false
}
func (noOpHolidays) AdjustForNonWorkingDaysWithReason(d time.Time, _, _ string) (time.Time, time.Time, bool, *AdjustmentReason) {
	return d, d, false, nil
}

type fixedCourts struct{}

func (fixedCourts) CountryRegime(_, _, _ string) (string, string, error) {
	return CountryDE, RegimeUPC, nil
}

func TestCalculate_BeforeChildOfCourtSetParent_OutOfOrderSequence(t *testing.T) {
	ctx := context.Background()

	// proceeding metadata
	jurisdiction := "UPC"
	procID := 1
	pt := ProceedingType{
		ID:           procID,
		Code:         "upc.inf.cfi",
		Name:         "Verletzungsverfahren",
		NameEN:       "Infringement",
		Jurisdiction: &jurisdiction,
		IsActive:     true,
	}

	mkID := func() uuid.UUID {
		id, _ := uuid.NewRandom()
		return id
	}
	str := func(s string) *string { return &s }
	procIDPtr := &procID

	socID := mkID()
	oralID := mkID()
	transID := mkID()
	interpID := mkID()

	socCode := "upc.inf.cfi.soc"
	oralCode := "upc.inf.cfi.oral"
	transCode := "upc.inf.cfi.translation_request"
	interpCode := "upc.inf.cfi.interpreter_cost"

	rules := []Rule{
		{
			ID:               socID,
			ProceedingTypeID: procIDPtr,
			ParentID:         nil,
			SubmissionCode:   &socCode,
			Name:             "Klageerhebung",
			NameEN:           "Statement of Claim",
			PrimaryParty:     str("claimant"),
			DurationValue:    0,
			DurationUnit:     "months",
			Timing:           str("after"),
			SequenceOrder:    0,
			IsCourtSet:       false,
			IsActive:         true,
			LifecycleState:   "published",
			Priority:         "mandatory",
		},
		// Translation request: sequence_order BEFORE the oral hearing.
		// Reproduces the real corpus ordering (DB rows 45 < 50).
		{
			ID:               transID,
			ProceedingTypeID: procIDPtr,
			ParentID:         &oralID,
			SubmissionCode:   &transCode,
			Name:             "Antrag auf Simultanübersetzung",
			NameEN:           "Translation request",
			PrimaryParty:     str("both"),
			DurationValue:    1,
			DurationUnit:     "months",
			Timing:           str("before"),
			SequenceOrder:    45,
			IsCourtSet:       false,
			IsActive:         true,
			LifecycleState:   "published",
			Priority:         "optional",
		},
		// Interpreter cost notice: sequence_order BEFORE the oral hearing.
		{
			ID:               interpID,
			ProceedingTypeID: procIDPtr,
			ParentID:         &oralID,
			SubmissionCode:   &interpCode,
			Name:             "Mitteilung Dolmetscherkosten",
			NameEN:           "Interpreter cost notice",
			PrimaryParty:     str("court"),
			DurationValue:    2,
			DurationUnit:     "weeks",
			Timing:           str("before"),
			SequenceOrder:    46,
			IsCourtSet:       false,
			IsActive:         true,
			LifecycleState:   "published",
			Priority:         "mandatory",
		},
		// Oral hearing: court-set, no calculable date. Listed AFTER its
		// "before"-timed children in sequence_order.
		{
			ID:               oralID,
			ProceedingTypeID: procIDPtr,
			ParentID:         nil,
			SubmissionCode:   &oralCode,
			Name:             "Mündliche Verhandlung",
			NameEN:           "Oral hearing",
			PrimaryParty:     str("court"),
			DurationValue:    0,
			DurationUnit:     "months",
			Timing:           str("after"),
			SequenceOrder:    50,
			IsCourtSet:       true,
			IsActive:         true,
			LifecycleState:   "published",
			Priority:         "mandatory",
		},
	}

	cat := &stubCatalog{pt: pt, rules: rules}

	// IncludeOptional=true because translation_request carries
	// priority='optional'; the test exercises the before-child-of-
	// court-set-parent flow, which is orthogonal to the optional-rule
	// suppression added in t-paliad-342.
	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
	}

	// The trigger event itself is unambiguous.
	if got := byCode[socCode]; got.DueDate != "2026-05-26" || !got.IsRootEvent {
		t.Errorf("SoC: DueDate=%q IsRootEvent=%v, want 2026-05-26 + IsRootEvent=true", got.DueDate, got.IsRootEvent)
	}

	// Oral hearing must surface as IsCourtSet (no date).
	oral := byCode[oralCode]
	if oral.DueDate != "" || !oral.IsCourtSet {
		t.Errorf("oral: DueDate=%q IsCourtSet=%v, want empty + IsCourtSet=true", oral.DueDate, oral.IsCourtSet)
	}

	// The two "before" children of the court-set oral hearing MUST surface
	// as conditional rows (no date, no fabricated arithmetic off the
	// trigger date). The buggy behaviour produces 2026-04-27 and 2026-05-12.
	trans := byCode[transCode]
	if trans.DueDate != "" {
		t.Errorf("translation_request: DueDate=%q, want empty (parent oral is court-set, no anchor known yet)", trans.DueDate)
	}
	if !trans.IsConditional && !trans.IsCourtSet {
		t.Errorf("translation_request: IsConditional=%v IsCourtSet=%v, want at least one true", trans.IsConditional, trans.IsCourtSet)
	}

	interp := byCode[interpCode]
	if interp.DueDate != "" {
		t.Errorf("interpreter_cost: DueDate=%q, want empty (parent oral is court-set, no anchor known yet)", interp.DueDate)
	}
	if !interp.IsConditional && !interp.IsCourtSet {
		t.Errorf("interpreter_cost: IsConditional=%v IsCourtSet=%v, want at least one true", interp.IsConditional, interp.IsCourtSet)
	}
}

// TestCalculate_BeforeChildOfCourtSetParent_WithOverride pins the
// override semantics: when the user supplies an anchor override for
// the court-set parent, the "before" children should compute against
// that override date instead of remaining conditional.
func TestCalculate_BeforeChildOfCourtSetParent_WithOverride(t *testing.T) {
	ctx := context.Background()

	jurisdiction := "UPC"
	procID := 1
	pt := ProceedingType{
		ID:           procID,
		Code:         "upc.inf.cfi",
		Name:         "Verletzungsverfahren",
		Jurisdiction: &jurisdiction,
		IsActive:     true,
	}

	mkID := func() uuid.UUID {
		id, _ := uuid.NewRandom()
		return id
	}
	str := func(s string) *string { return &s }
	procIDPtr := &procID

	socID := mkID()
	oralID := mkID()
	transID := mkID()

	socCode := "upc.inf.cfi.soc"
	oralCode := "upc.inf.cfi.oral"
	transCode := "upc.inf.cfi.translation_request"

	rules := []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",
		},
		{
			ID: transID, ProceedingTypeID: procIDPtr, ParentID: &oralID,
			SubmissionCode: &transCode, Name: "Antrag auf Simultanübersetzung", NameEN: "Translation request",
			PrimaryParty: str("both"), DurationValue: 1, DurationUnit: "months", Timing: str("before"),
			SequenceOrder: 45, IsActive: true, LifecycleState: "published", Priority: "optional",
		},
		{
			ID: oralID, ProceedingTypeID: procIDPtr, ParentID: nil,
			SubmissionCode: &oralCode, Name: "Mündliche Verhandlung", NameEN: "Oral hearing",
			PrimaryParty: str("court"), DurationValue: 0, DurationUnit: "months", Timing: str("after"),
			SequenceOrder: 50, IsCourtSet: true, IsActive: true, LifecycleState: "published", Priority: "mandatory",
		},
	}

	cat := &stubCatalog{pt: pt, rules: rules}

	// User pins the oral hearing to 2026-10-15. IncludeOptional=true
	// because translation_request is priority='optional' (t-paliad-342).
	opts := CalcOptions{
		IncludeOptional: true,
		AnchorOverrides: map[string]string{
			oralCode: "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
	}

	if got := byCode[oralCode].DueDate; got != "2026-10-15" {
		t.Errorf("oral: DueDate=%q, want 2026-10-15 (user override)", got)
	}

	// 1 month before 2026-10-15 = 2026-09-15
	if got := byCode[transCode].DueDate; got != "2026-09-15" {
		t.Errorf("translation_request: DueDate=%q, want 2026-09-15 (1 month before oral override)", got)
	}
}
