package service

import (
	"errors"
	"strings"
	"testing"
	"time"

	complaint "wucher/internal/domain/complaint"
	fleetstatus "wucher/internal/domain/fleet_status"
	"wucher/internal/domain/helicopter"
	helicopterusage "wucher/internal/domain/helicopter_usage"
	"wucher/internal/shared/pkg/nextdue"
)

func ptr[T any](v T) *T { return &v }

func TestDecideAutoAOG(t *testing.T) {
	now := time.Date(2026, 6, 17, 8, 0, 0, 0, time.UTC)
	reasonFn := func(c complaint.Complaint) string { return "complaint:" + c.Description }
	noComplaints := func() ([]complaint.Complaint, error) { return nil, nil }

	t.Run("next due grounds; complaints not consulted", func(t *testing.T) {
		loaderCalled := false
		loader := func() ([]complaint.Complaint, error) { loaderCalled = true; return nil, nil }
		evals := []NextDueEvaluation{{InspectionType: "A/F", AOG: true, Reason: "A/F due hours reached"}}
		grounded, reason, source, err := decideAutoAOG(evals, now, loader, reasonFn)
		if err != nil || !grounded {
			t.Fatalf("expected grounded, got grounded=%v err=%v", grounded, err)
		}
		if source != helicopter.AOGSourceAuto {
			t.Fatalf("source = %q, want auto", source)
		}
		if !strings.HasPrefix(reason, autoAOGReasonPrefixNextDue) {
			t.Fatalf("reason %q missing next-due prefix", reason)
		}
		if loaderCalled {
			t.Fatal("next-due grounded -> complaints must not be loaded (short-circuit)")
		}
	})

	t.Run("complaint grounds when no next due", func(t *testing.T) {
		classified := now.Add(-time.Hour)
		loader := func() ([]complaint.Complaint, error) {
			return []complaint.Complaint{{Description: "blade crack", MELSeverity: complaint.MELSeverityNonMEL, ReportedAt: now.Add(-time.Hour), MELClassifiedAt: &classified}}, nil
		}
		grounded, reason, source, err := decideAutoAOG(nil, now, loader, reasonFn)
		if err != nil || !grounded {
			t.Fatalf("expected grounded, got grounded=%v err=%v", grounded, err)
		}
		if source != helicopter.AOGSourceComplaint {
			t.Fatalf("source = %q, want complaint", source)
		}
		if reason != "complaint:blade crack" {
			t.Fatalf("reason = %q, want injected complaint reason", reason)
		}
	})

	t.Run("nothing grounds", func(t *testing.T) {
		grounded, _, _, err := decideAutoAOG(nil, now, noComplaints, reasonFn)
		if err != nil || grounded {
			t.Fatalf("expected not grounded, got grounded=%v err=%v", grounded, err)
		}
	})

	t.Run("loader error propagates", func(t *testing.T) {
		loader := func() ([]complaint.Complaint, error) { return nil, errors.New("db down") }
		if _, _, _, err := decideAutoAOG(nil, now, loader, reasonFn); err == nil {
			t.Fatal("expected loader error to propagate")
		}
	})
}

func assertFloatPtr(t *testing.T, label string, got, want *float64) {
	t.Helper()
	if want == nil {
		if got != nil {
			t.Errorf("%s = %v, want nil", label, *got)
		}
		return
	}
	if got == nil {
		t.Errorf("%s = nil, want %v", label, *want)
		return
	}
	const eps = 1e-9
	if diff := *got - *want; diff < -eps || diff > eps {
		t.Errorf("%s = %v, want %v", label, *got, *want)
	}
}

func TestBuildFleetStatusSummaryEngineGating(t *testing.T) {
	u := &helicopterusage.HelicopterUsage{TotalAirframeHours: 10}
	got := buildFleetStatusSummary(u, false, false)
	assertFloatPtr(t, "airframe.hours", got.Airframe.Hours, ptr(10.0))
	if got.Engine1.Hours != nil || got.Engine2.Hours != nil {
		t.Errorf("expected empty engines, got %+v / %+v", got.Engine1, got.Engine2)
	}
}

func TestBuildFleetStatusSummaryFromUsage(t *testing.T) {
	u := &helicopterusage.HelicopterUsage{
		TotalLanding:         10,
		TotalAirframeHours:   3,
		TotalAirframeCycles:  20,
		TotalEngine1Hours:    8011,
		TotalEngine1GpcNgN1:  101,
		TotalEngine1PtcNfN2:  102,
		TotalEngine1Ccc:      7,
		TotalEngine2Hours:    5981,
		TotalRotorBrakeCycle: 4,
		TotalHookRelease:     2,
	}
	got := buildFleetStatusSummary(u, true, false)
	assertFloatPtr(t, "airframe.hours", got.Airframe.Hours, ptr(3.0))
	assertFloatPtr(t, "airframe.cycles", got.Airframe.Cycles, ptr(20.0))
	assertFloatPtr(t, "engine1.hours", got.Engine1.Hours, ptr(8011.0))
	assertFloatPtr(t, "engine1.gpc_ng_n1", got.Engine1.GpcNgN1, ptr(101.0))
	assertFloatPtr(t, "engine1.ptc_nf_n2", got.Engine1.PtcNfN2, ptr(102.0))
	assertFloatPtr(t, "engine1.ccc", got.Engine1.Ccc, ptr(7.0))
	if got.Engine2.Hours != nil {
		t.Errorf("engine 2 absent (NR2 false) -> hours must be nil, got %v", *got.Engine2.Hours)
	}
	assertFloatPtr(t, "landings", got.Landings, ptr(10.0))
	assertFloatPtr(t, "rotor_brake", got.RotorBrakeCycles, ptr(4.0))
	assertFloatPtr(t, "hook_releases", got.HookReleases, ptr(2.0))
}

func TestDerefFloat(t *testing.T) {
	if derefFloat(nil) != 0 {
		t.Fatal("nil should deref to 0")
	}
	if derefFloat(ptr(12.5)) != 12.5 {
		t.Fatal("deref value mismatch")
	}
}

func TestEvaluateNextDueSchedulesPerComponent(t *testing.T) {
	now := time.Date(2026, 6, 17, 8, 0, 0, 0, time.UTC)
	schedules := []fleetstatus.MaintenanceSchedule{
		{InspectionType: fleetstatus.InspectionTypeAF, Due: "100"},
		{InspectionType: fleetstatus.InspectionTypeENG1, Due: "100", Ext: "120"},
		{InspectionType: fleetstatus.InspectionTypeENG2, Due: "100"},
	}
	hours := ComponentHours{Airframe: 150, Engine1: 110, Engine2: 90}

	// Engine 2 present: ENG2 (90 < 100) not overdue; A/F (150>100) overdue; ENG1 (110<120 ext) not overdue.
	evals, err := EvaluateNextDueSchedules(schedules, hours, true, true, true, now)
	if err != nil {
		t.Fatalf("err: %v", err)
	}
	got := map[string]bool{}
	for _, e := range evals {
		got[e.InspectionType] = e.AOG
	}
	if !got[fleetstatus.InspectionTypeAF] {
		t.Error("A/F should be overdue (150 > 100)")
	}
	if got[fleetstatus.InspectionTypeENG1] {
		t.Error("ENG1 should NOT be overdue (110 < 120 ext)")
	}
	if got[fleetstatus.InspectionTypeENG2] {
		t.Error("ENG2 should NOT be overdue (90 < 100)")
	}

	// Engine 2 absent: an ENG2 schedule must be skipped even if hours exceed the limit.
	hours.Engine2 = 999
	evals2, err := EvaluateNextDueSchedules(schedules, hours, true, true, false, now)
	if err != nil {
		t.Fatalf("err: %v", err)
	}
	for _, e := range evals2 {
		if e.InspectionType == fleetstatus.InspectionTypeENG2 && e.AOG {
			t.Error("absent engine 2 must not ground the aircraft")
		}
	}
}

func TestEvaluateInspectionDueHours(t *testing.T) {
	now := time.Date(2026, 6, 17, 8, 0, 0, 0, time.UTC)
	p := nextdue.NewParser()
	cases := []struct {
		name    string
		due     string
		ext     string
		current float64
		wantAOG bool
	}{
		{name: "below due", due: "100", ext: "", current: 99, wantAOG: false},
		{name: "exactly at due not overdue", due: "100", ext: "", current: 100, wantAOG: false},
		{name: "past due", due: "100", ext: "", current: 101, wantAOG: true},
		{name: "ext extends beyond due", due: "100", ext: "120", current: 110, wantAOG: false},
		{name: "past ext", due: "100", ext: "120", current: 121, wantAOG: true},
		{name: "ext zero treated as unset", due: "100", ext: "0", current: 101, wantAOG: true},
		{name: "unset", due: "", ext: "", current: 9999, wantAOG: false},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			ev, err := evaluateInspectionDue(p, "A/F", tc.due, tc.ext, tc.current, true, now)
			if err != nil {
				t.Fatalf("unexpected err: %v", err)
			}
			if ev.AOG != tc.wantAOG {
				t.Fatalf("AOG = %v (reason %q), want %v", ev.AOG, ev.Reason, tc.wantAOG)
			}
		})
	}
}

func TestEvaluateInspectionDueDate(t *testing.T) {
	now := time.Date(2026, 6, 17, 8, 0, 0, 0, time.UTC)
	p := nextdue.NewParser()
	cases := []struct {
		name    string
		due     string
		ext     string
		wantAOG bool
	}{
		{name: "due date passed", due: "2026-06-10", ext: "", wantAOG: true},
		{name: "due date today not yet overdue", due: "2026-06-17", ext: "", wantAOG: false},
		{name: "due date future", due: "2026-06-20", ext: "", wantAOG: false},
		{name: "ext date extends beyond passed due", due: "2026-06-10", ext: "2026-06-25", wantAOG: false},
		{name: "past ext date", due: "2026-06-10", ext: "2026-06-12", wantAOG: true},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			ev, err := evaluateInspectionDue(p, "MAIN1", tc.due, tc.ext, 0, true, now)
			if err != nil {
				t.Fatalf("unexpected err: %v", err)
			}
			if ev.AOG != tc.wantAOG {
				t.Fatalf("AOG = %v (reason %q), want %v", ev.AOG, ev.Reason, tc.wantAOG)
			}
		})
	}
}

func TestEvaluateInspectionDueInvalid(t *testing.T) {
	p := nextdue.NewParser()
	if _, err := evaluateInspectionDue(p, "A/F", "100 FH", "", 0, true, time.Now()); err == nil {
		t.Fatal("expected parse error for due '100 FH'")
	}
	if _, err := evaluateInspectionDue(p, "A/F", "100", "bad ext", 0, true, time.Now()); err == nil {
		t.Fatal("expected parse error for ext 'bad ext'")
	}
}

func TestEvaluateInspectionDueNoUsageData(t *testing.T) {
	p := nextdue.NewParser()
	now := time.Date(2026, 6, 17, 8, 0, 0, 0, time.UTC)

	ev, err := evaluateInspectionDue(p, "A/F", "100", "", 150, false, now)
	if err != nil {
		t.Fatalf("err: %v", err)
	}
	if ev.AOG {
		t.Fatal("hours due must not ground when there is no usage data yet")
	}

	evDate, err := evaluateInspectionDue(p, "MAIN1", "2026-06-10", "", 0, false, now)
	if err != nil {
		t.Fatalf("err: %v", err)
	}
	if !evDate.AOG {
		t.Fatal("a passed date due should still ground regardless of usage data")
	}
}
