package router

import (
	"context"
	"os/exec"
	"testing"
	"time"
)

// probeNow is the clock the five sentences below were measured against on the
// box at 03:53 on 2026-08-06, right after #252 deployed. Three of them wrote a
// reminder for 03:53 itself, which is the current minute and not an hour anyone
// said (V-610).
var probeNow = time.Date(2026, 8, 6, 3, 53, 0, 0, time.Local)

// kProbe — the five sentences, with what each must resolve to. The two that
// already worked are here so that fixing "к" cannot cost "в".
var kProbe = []struct {
	text string
	// day is the offset from probeNow's date, hour is the fire hour.
	day     int
	hour    int
	whyItIs string
}{
	{
		text:    "напомни завтра в три часа дня позвонить врачу",
		day:     1,
		hour:    15,
		whyItIs: "в plus a spoken hour and a qualifier resolves, and did before #252",
	},
	{
		text:    "напомни завтра в 15:00 позвонить врачу",
		day:     1,
		hour:    15,
		whyItIs: "a written clock resolves, and did before #252",
	},
	{
		text:    "напомни завтра к трём часам дня позвонить врачу",
		day:     1,
		hour:    15,
		whyItIs: "к names the same hour в does, and wrote 03:53 after #252",
	},
	{
		text:    "напомни сегодня к пяти часам вечера позвонить врачу",
		day:     0,
		hour:    17,
		whyItIs: "the same defect on today and on the oblique пяти",
	},
	{
		text:    "напомни завтра к трём часам позвонить врачу",
		day:     1,
		hour:    3,
		whyItIs: "no qualifier, so the hour reads as spoken and the daemon asks which half",
	},
}

// TestKPrepositionHourStub — the probe against the floor parser, which answers
// on every box whether or not python is installed.
func TestKPrepositionHourStub(t *testing.T) {
	ex := Extractor{Time: StubDateTimeParser{}}
	for _, c := range kProbe {
		t.Run(c.text, func(t *testing.T) {
			got := ex.Extract(context.Background(), IntentReminder, c.text, probeNow)
			if !got.HasTime {
				t.Fatalf("time slot empty: %s", c.whyItIs)
			}
			checkFire(t, got.Time, c.day, c.hour, c.whyItIs)
		})
	}
}

// TestKPrepositionHourPython — the same probe against the production parser.
// Skips where python3 or dateparser is missing, as the rest of this package's
// python tests do.
func TestKPrepositionHourPython(t *testing.T) {
	requirePython(t)
	p := NewPythonDateParser()
	ex := Extractor{Time: p}
	for _, c := range kProbe {
		t.Run(c.text, func(t *testing.T) {
			got := ex.Extract(context.Background(), IntentReminder, c.text, probeNow)
			if !got.HasTime {
				t.Fatalf("time slot empty: %s", c.whyItIs)
			}
			checkFire(t, got.Time, c.day, c.hour, c.whyItIs)
		})
	}
}

func checkFire(t *testing.T, fire time.Time, dayOffset, hour int, why string) {
	t.Helper()
	if fire.Hour() != hour || fire.Minute() != 0 {
		t.Errorf("fire = %s, want %02d:00 — %s", fire.Format("2006-01-02 15:04"), hour, why)
	}
	if fire.Minute() == probeNow.Minute() && fire.Hour() == probeNow.Hour() {
		t.Errorf("fire = %s, which is the clock at the moment of the turn and not an hour he said", fire.Format("15:04"))
	}
	want := probeNow.AddDate(0, 0, dayOffset)
	if fire.Year() != want.Year() || fire.Month() != want.Month() || fire.Day() != want.Day() {
		t.Errorf("fire = %s, want the %s — %s", fire.Format("2006-01-02"), want.Format("2006-01-02"), why)
	}
}

// TestUnresolvedHourLeavesTheSlotEmpty — the durable half. A parser that read
// the day and took the minute off the clock must not fill the time slot, no
// matter which preposition lost the hour. This is the whole class the "к" case
// was one member of.
func TestUnresolvedHourLeavesTheSlotEmpty(t *testing.T) {
	ex := Extractor{Time: clockEchoParser{}}
	for _, s := range []string{
		"напомни завтра к трём часам дня позвонить врачу",
		"напомни завтра в три часа дня позвонить врачу",
		"напомни завтра на девять позвонить врачу",
		"напомни сегодня к пяти часам вечера позвонить врачу",
	} {
		got := ex.Extract(context.Background(), IntentReminder, s, probeNow)
		if got.HasTime {
			t.Errorf("Extract(%q) filled the slot with %s, which is the current minute; she has to ask", s, got.Time.Format("15:04"))
		}
	}
}

// TestSpokenMinutesSurviveTheRefusal — the three shapes that name a minute of
// their own, and an hour that happens to be the hour it is spoken in. Refusing
// on the whole instant instead of on the minute would cost every one of these,
// and ru-rem-006 in the routing fixture is the case that says so.
func TestSpokenMinutesSurviveTheRefusal(t *testing.T) {
	noon := time.Date(2026, 7, 30, 12, 0, 0, 0, time.UTC)
	ex := Extractor{Time: StubDateTimeParser{}}
	for _, c := range []struct {
		text string
		hour int
		min  int
	}{
		{"напомни послезавтра в 12 забрать заказ", 12, 0},
		{"разбуди меня в 6:30", 6, 30},
		{"напомни в половине восьмого выпить таблетку", 7, 30},
		{"напомни без четверти восемь выходить", 7, 45},
	} {
		got := ex.Extract(context.Background(), IntentReminder, c.text, noon)
		if !got.HasTime {
			t.Errorf("Extract(%q) left the slot empty; he said the time", c.text)
			continue
		}
		if got.Time.Hour() != c.hour || got.Time.Minute() != c.min {
			t.Errorf("Extract(%q) = %s, want %02d:%02d", c.text, got.Time.Format("15:04"), c.hour, c.min)
		}
	}
}

// TestIntervalKeepsTheCurrentMinute — an interval is measured from now and may
// land on now's own minute, so the refusal above must not reach it.
func TestIntervalKeepsTheCurrentMinute(t *testing.T) {
	ex := Extractor{Time: StubDateTimeParser{}}
	got := ex.Extract(context.Background(), IntentReminder, "напомни через час позвонить врачу", probeNow)
	if !got.HasTime {
		t.Fatal("через час names one instant and answers the hour and the day together")
	}
	if want := probeNow.Add(time.Hour); !got.Time.Equal(want) {
		t.Errorf("fire = %s, want %s", got.Time.Format("15:04"), want.Format("15:04"))
	}
}

func requirePython(t *testing.T) {
	t.Helper()
	if _, err := exec.LookPath("python3"); err != nil {
		t.Skip("python3 not on PATH — skipping dateparser tests")
	}
	if err := exec.Command("python3", "-c", "import dateparser").Run(); err != nil {
		t.Skip("python dateparser not installed — skipping dateparser tests")
	}
}
