package router

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

// TestPythonDateParser requires python3 + dateparser installed. Skips
// gracefully in environments without them (e.g. bare go test, dev machines
// without the Docker runtime image).
func TestPythonDateParser(t *testing.T) {
	// Skip if python3 isn't on PATH.
	if _, err := exec.LookPath("python3"); err != nil {
		t.Skip("python3 not on PATH — skipping dateparser tests")
	}
	// Skip if dateparser isn't installed.
	cmd := exec.Command("python3", "-c", "import dateparser")
	if err := cmd.Run(); err != nil {
		t.Skip("python dateparser not installed — skipping dateparser tests")
	}

	p := NewPythonDateParser()
	now := time.Date(2026, 7, 6, 18, 0, 0, 0, time.Local)
	ctx := context.Background()

	tests := []struct {
		name   string
		text   string
		wantOK bool
		checkT func(t *testing.T, got, now time.Time)
	}{
		{
			name:   "ru relative — через час",
			text:   "напомни через час выпить воды",
			wantOK: true,
			checkT: func(t *testing.T, got, now time.Time) {
				d := got.Sub(now)
				if d < 50*time.Minute || d > 70*time.Minute {
					t.Errorf("через час: got %v from now, want ~1h", d)
				}
			},
		},
		{
			name:   "ru relative — через 30 минут",
			text:   "напомни через 30 минут снять бельё",
			wantOK: true,
			checkT: func(t *testing.T, got, now time.Time) {
				d := got.Sub(now)
				if d < 25*time.Minute || d > 35*time.Minute {
					t.Errorf("через 30 минут: got %v from now, want ~30m", d)
				}
			},
		},
		{
			name:   "ru absolute — завтра в 9 утра",
			text:   "напомни завтра в 9 утра позвонить",
			wantOK: true,
			checkT: func(t *testing.T, got, now time.Time) {
				if got.Hour() != 9 {
					t.Errorf("завтра в 9 утра: hour=%d, want 9", got.Hour())
				}
				if got.Sub(now) < 12*time.Hour {
					t.Errorf("завтра в 9 утра: only %v from now, want >12h (tomorrow)", got.Sub(now))
				}
			},
		},
		{
			name:   "ru complex — в следующую пятницу",
			text:   "напомни в следующую пятницу оплатить счёт",
			wantOK: true,
			checkT: func(t *testing.T, got, now time.Time) {
				if got.Weekday() != time.Friday {
					t.Errorf("в следующую пятницу: weekday=%v, want Friday", got.Weekday())
				}
				if got.Sub(now) < 24*time.Hour {
					t.Errorf("в следующую пятницу: only %v from now, want >24h (future)", got.Sub(now))
				}
			},
		},
		{
			name:   "en relative — in 30 minutes",
			text:   "remind me in 30 minutes to drink water",
			wantOK: true,
			checkT: func(t *testing.T, got, now time.Time) {
				d := got.Sub(now)
				if d < 25*time.Minute || d > 35*time.Minute {
					t.Errorf("in 30 minutes: got %v from now, want ~30m", d)
				}
			},
		},
		{
			name:   "en absolute — tomorrow at 8am",
			text:   "remind me tomorrow at 8am to call the doctor",
			wantOK: true,
			checkT: func(t *testing.T, got, now time.Time) {
				if got.Hour() != 8 {
					t.Errorf("tomorrow at 8am: hour=%d, want 8", got.Hour())
				}
				if got.Sub(now) < 12*time.Hour {
					t.Errorf("tomorrow at 8am: only %v from now, want >12h (tomorrow)", got.Sub(now))
				}
			},
		},
		// A bare hour after a day word used to be dropped, and the current
		// clock carried onto that day: at 21:12 "завтра в семь" confirmed a
		// reminder for 21:12 tomorrow (Vikunja #551). She invented a time
		// instead of asking, on a path that then fires.
		{
			name:   "ru bare hour — завтра в семь",
			text:   "напомни мне завтра в семь позвонить маме",
			wantOK: true,
			checkT: func(t *testing.T, got, now time.Time) {
				if got.Hour() != 7 || got.Minute() != 0 {
					t.Errorf("завтра в семь: %02d:%02d, want 07:00", got.Hour(), got.Minute())
				}
			},
		},
		{
			// "7 часов" was read as seven hours from now, which also moved the day.
			name:   "ru bare hour — завтра в 7 часов",
			text:   "напомни завтра в 7 часов позвонить",
			wantOK: true,
			checkT: func(t *testing.T, got, now time.Time) {
				if got.Hour() != 7 || got.Minute() != 0 {
					t.Errorf("завтра в 7 часов: %02d:%02d, want 07:00", got.Hour(), got.Minute())
				}
			},
		},
		{
			name:   "en bare hour — tomorrow at 7",
			text:   "remind me tomorrow at 7 to call mum",
			wantOK: true,
			checkT: func(t *testing.T, got, now time.Time) {
				if got.Hour() != 7 || got.Minute() != 0 {
					t.Errorf("tomorrow at 7: %02d:%02d, want 07:00", got.Hour(), got.Minute())
				}
			},
		},
		{
			// The rewrite must not touch a duration: "через 2 часа" is not "в 2".
			name:   "ru duration is untouched — через 2 часа",
			text:   "напомни через 2 часа выпить воды",
			wantOK: true,
			checkT: func(t *testing.T, got, now time.Time) {
				d := got.Sub(now)
				if d < 110*time.Minute || d > 130*time.Minute {
					t.Errorf("через 2 часа: got %v from now, want ~2h", d)
				}
			},
		},
		{
			name:   "no date — напомни мне",
			text:   "напомни мне",
			wantOK: false,
		},
		{
			name:   "no date — remind me",
			text:   "remind me to do something",
			wantOK: false,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, ok, err := p.Parse(ctx, tt.text, now)
			if err != nil {
				t.Fatalf("Parse(%q) error: %v", tt.text, err)
			}
			if ok != tt.wantOK {
				t.Fatalf("Parse(%q) ok=%v, want %v (got time=%v)", tt.text, ok, tt.wantOK, got)
			}
			if ok && tt.checkT != nil {
				tt.checkT(t, got, now)
			}
		})
	}
}

// TestPythonDateParser_FallbackToStub verifies that when python3 is not
// available, the parser falls back to StubDateTimeParser instead of
// returning an error.
func TestPythonDateParser_FallbackToStub(t *testing.T) {
	if _, err := exec.LookPath("python3"); err == nil {
		t.Skip("python3 is available — fallback path not exercised")
	}
	p := NewPythonDateParser()
	ctx := context.Background()
	now := time.Date(2026, 7, 6, 18, 0, 0, 0, time.Local)

	// The stub handles "in 1 hour" — should still work via fallback.
	got, ok, err := p.Parse(ctx, "in 1 hour", now)
	if err != nil {
		t.Fatalf("fallback Parse error: %v", err)
	}
	if !ok {
		t.Fatal("fallback Parse: ok=false, want true (stub should handle 'in 1 hour')")
	}
	d := got.Sub(now)
	if d < 50*time.Minute || d > 70*time.Minute {
		t.Errorf("fallback 'in 1 hour': got %v from now, want ~1h", d)
	}
}
