package litigationplanner

import (
	"context"
	"testing"

	"github.com/google/uuid"
)

// TestAppealFilerRole pins the rule-semantic mapping that drives
// column-bucketing on the unified upc.apl Berufung timeline
// (t-paliad-307 / m/paliad#136 Bug 1). Every appeal filing rule has
// primary_party='both' in the catalog so the bucketer can't decide
// between Berufungskläger and Berufungsbeklagter columns from
// primary_party alone — the appeal role fills that gap.
func TestAppealFilerRole(t *testing.T) {
	cases := []struct {
		code string
		want string
	}{
		// Appellant filings (Berufungskläger initiates / replies to cross).
		{"upc.apl.merits.notice", AppealRoleAppellant},
		{"upc.apl.merits.grounds", AppealRoleAppellant},
		{"upc.apl.merits.cross_a_reply", AppealRoleAppellant},
		{"upc.apl.cost.leave_app", AppealRoleAppellant},
		{"upc.apl.order.with_leave", AppealRoleAppellant},
		{"upc.apl.order.grounds_orders", AppealRoleAppellant},
		{"upc.apl.order.discretion", AppealRoleAppellant},
		{"upc.apl.order.cross_reply", AppealRoleAppellant},
		// Appellee filings (Berufungsbeklagter responds + cross-appeals).
		{"upc.apl.merits.response", AppealRoleAppellee},
		{"upc.apl.merits.cross_a", AppealRoleAppellee},
		{"upc.apl.order.response_orders", AppealRoleAppellee},
		{"upc.apl.order.cross", AppealRoleAppellee},
		// Court-issued events stay empty — they route on party='court'.
		{"upc.apl.merits.decision", ""},
		{"upc.apl.merits.oral", ""},
		{"upc.apl.cost.decision", ""},
		{"upc.apl.order.order", ""},
		// Unmapped codes are empty (defensive — never silently picks a
		// side for a new appeal rule we forgot to map).
		{"upc.inf.cfi.soc", ""},
		{"", ""},
		{"foo.bar", ""},
	}
	for _, c := range cases {
		if got := AppealFilerRole(c.code); got != c.want {
			t.Errorf("AppealFilerRole(%q) = %q, want %q", c.code, got, c.want)
		}
	}
}

// TestCalculate_AppealSyntheticTriggerRow exercises the synthetic root
// row the engine prepends when CalcOptions.AppealTarget is set
// (t-paliad-307 / m/paliad#136 Bug 2). The row carries the
// per-appeal-target label, the trigger date as DueDate, IsRootEvent=
// IsTriggerEvent=true, and party=court. Without the appeal_target
// filter, no synthetic row is emitted (regression guard).
func TestCalculate_AppealSyntheticTriggerRow(t *testing.T) {
	ctx := context.Background()

	jurisdiction := "UPC"
	procID := 1
	pt := ProceedingType{
		ID:           procID,
		Code:         "upc.apl.unified",
		Name:         "Berufung",
		NameEN:       "Appeal",
		Jurisdiction: &jurisdiction,
		IsActive:     true,
	}

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

	noticeCode := "upc.apl.merits.notice"
	groundsCode := "upc.apl.merits.grounds"

	rules := []Rule{
		{
			ID:               mkID(),
			ProceedingTypeID: procIDPtr,
			SubmissionCode:   &noticeCode,
			Name:             "Berufungseinlegung",
			NameEN:           "Notice of Appeal",
			PrimaryParty:     str(PrimaryPartyBoth),
			DurationValue:    2,
			DurationUnit:     "months",
			Timing:           str("after"),
			SequenceOrder:    0,
			IsActive:         true,
			LifecycleState:   "published",
			Priority:         "mandatory",
			AppliesToTarget:  []string{AppealTargetEndentscheidung, AppealTargetSchadensbemessung},
		},
		{
			ID:               mkID(),
			ProceedingTypeID: procIDPtr,
			SubmissionCode:   &groundsCode,
			Name:             "Berufungsbegründung",
			NameEN:           "Statement of Grounds",
			PrimaryParty:     str(PrimaryPartyBoth),
			DurationValue:    4,
			DurationUnit:     "months",
			Timing:           str("after"),
			SequenceOrder:    1,
			IsActive:         true,
			LifecycleState:   "published",
			Priority:         "mandatory",
			AppliesToTarget:  []string{AppealTargetEndentscheidung, AppealTargetSchadensbemessung},
		},
	}

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

	t.Run("with appeal_target — synthetic row prepended + appeal_role stamped", func(t *testing.T) {
		opts := CalcOptions{AppealTarget: AppealTargetEndentscheidung}
		timeline, err := Calculate(ctx, "upc.apl.unified", "2026-05-26", opts, cat, noOpHolidays{}, fixedCourts{})
		if err != nil {
			t.Fatalf("Calculate: %v", err)
		}
		if len(timeline.Deadlines) < 3 {
			t.Fatalf("expected synthetic row + 2 rules, got %d rows", len(timeline.Deadlines))
		}
		// Synthetic row first.
		first := timeline.Deadlines[0]
		if !first.IsTriggerEvent {
			t.Errorf("first row IsTriggerEvent=%v, want true", first.IsTriggerEvent)
		}
		if !first.IsRootEvent {
			t.Errorf("first row IsRootEvent=%v, want true", first.IsRootEvent)
		}
		if first.Name != "Endentscheidung (R.118)" {
			t.Errorf("first row Name=%q, want %q", first.Name, "Endentscheidung (R.118)")
		}
		if first.NameEN != "Final decision (R.118)" {
			t.Errorf("first row NameEN=%q, want %q", first.NameEN, "Final decision (R.118)")
		}
		if first.DueDate != "2026-05-26" {
			t.Errorf("first row DueDate=%q, want 2026-05-26", first.DueDate)
		}
		if first.Party != PrimaryPartyCourt {
			t.Errorf("first row Party=%q, want court", first.Party)
		}
		// Real rules should carry AppealRole.
		byCode := map[string]TimelineEntry{}
		for _, d := range timeline.Deadlines {
			byCode[d.Code] = d
		}
		if got := byCode[noticeCode].AppealRole; got != AppealRoleAppellant {
			t.Errorf("notice AppealRole=%q, want appellant", got)
		}
		if got := byCode[groundsCode].AppealRole; got != AppealRoleAppellant {
			t.Errorf("grounds AppealRole=%q, want appellant", got)
		}
	})

	t.Run("without appeal_target — no synthetic row, no appeal_role", func(t *testing.T) {
		opts := CalcOptions{}
		timeline, err := Calculate(ctx, "upc.apl.unified", "2026-05-26", opts, cat, noOpHolidays{}, fixedCourts{})
		if err != nil {
			t.Fatalf("Calculate: %v", err)
		}
		for _, d := range timeline.Deadlines {
			if d.IsTriggerEvent {
				t.Errorf("unexpected synthetic trigger row when appeal_target is unset: %+v", d)
			}
			if d.AppealRole != "" {
				t.Errorf("unexpected AppealRole=%q when appeal_target is unset (rule %q)", d.AppealRole, d.Code)
			}
		}
	})

	t.Run("unknown appeal_target — short-circuits to no-op", func(t *testing.T) {
		opts := CalcOptions{AppealTarget: "bogus"}
		timeline, err := Calculate(ctx, "upc.apl.unified", "2026-05-26", opts, cat, noOpHolidays{}, fixedCourts{})
		if err != nil {
			t.Fatalf("Calculate: %v", err)
		}
		// IsValidAppealTarget("bogus") = false, so the engine skips
		// both the rule filter AND the synthetic trigger emission.
		for _, d := range timeline.Deadlines {
			if d.IsTriggerEvent {
				t.Errorf("unexpected synthetic trigger row for unknown target: %+v", d)
			}
		}
	})
}
