package main

import (
	"context"
	"errors"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"testing"
	"time"

	"github.com/kami/maven/internal/ipc"
)

// The page showed the storage envelope and the UTC instant (Vikunja #469).
func TestReminderRowsUnwrapAndLocalise(t *testing.T) {
	fire := time.Date(2026, 8, 4, 18, 30, 0, 0, time.UTC)
	rows := reminderRows([]ipc.Reminder{{
		ID:        17,
		CreatedTs: fire.Add(-time.Hour),
		FireTs:    fire,
		Status:    "pending",
		Payload:   `{"text":"выпить таблетки"}`,
	}})
	if len(rows) != 1 {
		t.Fatalf("rows = %d, want 1", len(rows))
	}
	if rows[0].Text != "выпить таблетки" {
		t.Errorf("Text = %q, want the words without the envelope", rows[0].Text)
	}
	if want := fire.Local().Format("02 Jan 15:04"); rows[0].Fires != want {
		t.Errorf("Fires = %q, want %q", rows[0].Fires, want)
	}
	if strings.Contains(rows[0].Text, "{") {
		t.Errorf("Text still carries JSON: %q", rows[0].Text)
	}
	if rows[0].ID != 17 || !rows[0].CanCancel {
		t.Errorf("pending reminder action binding = %+v, want id 17 cancellable", rows[0])
	}
}

func TestReminderRowsExposeBlockedDelivery(t *testing.T) {
	blocked := time.Date(2026, 8, 13, 8, 0, 0, 0, time.UTC)
	rows := reminderRows([]ipc.Reminder{{
		Status:               "pending",
		Payload:              `{"text":"позвонить врачу"}`,
		DeliveryBlockedTs:    blocked,
		DeliveryBlockedError: "ntfy credentials rejected",
	}})
	if len(rows) != 1 || rows[0].Status != "blocked" || rows[0].Detail != "ntfy credentials rejected" {
		t.Fatalf("blocked reminder is not visible: %+v", rows)
	}
	if !rows[0].CanCancel {
		t.Fatal("a blocked but still-pending reminder must remain cancellable")
	}
}

func TestReminderRowsShowTheCurrentRecurringOccurrence(t *testing.T) {
	original := time.Date(2026, 8, 1, 9, 0, 0, 0, time.UTC)
	next := time.Date(2026, 8, 16, 9, 0, 0, 0, time.UTC)
	rows := reminderRows([]ipc.Reminder{{
		ID: 42, FireTs: original, NextFireTs: next, Cron: "0 9 * * *",
		Status: "pending", Payload: `{"text":"принять лекарство"}`,
	}})
	if len(rows) != 1 || rows[0].Fires != next.Local().Format("02 Jan 15:04") {
		t.Fatalf("recurring fire = %+v, want current occurrence %s", rows, next)
	}
	if rows[0].Schedule != "recurring · 0 9 * * *" || !rows[0].CanCancel {
		t.Fatalf("recurring identity/action = %+v", rows[0])
	}
}

func TestReminderRowsOnlyPendingCanCancel(t *testing.T) {
	rows := reminderRows([]ipc.Reminder{{Status: "cancelled"}, {Status: "fired"}})
	for _, row := range rows {
		if row.CanCancel {
			t.Errorf("terminal row %+v exposed a cancel action", row)
		}
	}
}

// A payload that is not the envelope is his own words, so it is shown as it is.
func TestReminderTextKeepsPlainPayload(t *testing.T) {
	for _, tc := range []struct{ in, want string }{
		{`{"text":"позвонить маме"}`, "позвонить маме"},
		{"  полить цветы  ", "полить цветы"},
		{`{"body":"nope"}`, `{"body":"nope"}`},
		{"", ""},
	} {
		if got := reminderText(tc.in); got != tc.want {
			t.Errorf("reminderText(%q) = %q, want %q", tc.in, got, tc.want)
		}
	}
}

type reminderCore struct {
	ipc.UnimplementedCoreAPI

	reminders  []ipc.Reminder
	pending    []ipc.Reminder
	listErr    error
	pendingErr error
	cancelErr  error
	cancelID   int64
}

func (c *reminderCore) ListReminders(context.Context, int) ([]ipc.Reminder, error) {
	return c.reminders, c.listErr
}

func (c *reminderCore) ListPendingReminders(context.Context, int) ([]ipc.Reminder, error) {
	return c.pending, c.pendingErr
}

func (c *reminderCore) CancelReminder(_ context.Context, id int64) error {
	c.cancelID = id
	if c.cancelErr != nil {
		return c.cancelErr
	}
	for i := range c.reminders {
		if c.reminders[i].ID == id {
			c.reminders[i].Status = "cancelled"
		}
	}
	return nil
}

func reminderPost(action, id string) *http.Request {
	form := url.Values{"action": {action}, "id": {id}}
	req := httptest.NewRequest(http.MethodPost, "/reminders", strings.NewReader(form.Encode()))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	return req
}

func TestHandleRemindersCancel(t *testing.T) {
	core := &reminderCore{reminders: []ipc.Reminder{{
		ID: 23, Status: "pending", Payload: `{"text":"позвонить врачу"}`,
	}}}
	rr := httptest.NewRecorder()
	handleReminders(rr, reminderPost("cancel", "23"), core)

	if rr.Code != http.StatusSeeOther || rr.Header().Get("Location") != "/reminders?cancelled=1" {
		t.Fatalf("status/location = %d %q, want 303 PRG", rr.Code, rr.Header().Get("Location"))
	}
	if core.cancelID != 23 {
		t.Fatalf("cancel id = %d, want 23", core.cancelID)
	}

	rr = httptest.NewRecorder()
	handleReminders(rr, httptest.NewRequest(http.MethodGet, "/reminders?cancelled=1", nil), core)
	if rr.Code != http.StatusOK || !strings.Contains(rr.Body.String(), "reminder cancelled") || strings.Contains(rr.Body.String(), "value=\"23\"") {
		t.Fatalf("redirect outcome rendered incorrectly: status=%d body=%s", rr.Code, rr.Body.String())
	}
}

func TestHandleRemindersSuccessfulMutationDoesNotDependOnRefresh(t *testing.T) {
	core := &reminderCore{listErr: errors.New("offline"), pendingErr: errors.New("offline")}
	rr := httptest.NewRecorder()
	handleReminders(rr, reminderPost("cancel", "23"), core)
	if rr.Code != http.StatusSeeOther || core.cancelID != 23 {
		t.Fatalf("successful cancellation became refresh failure: status=%d id=%d body=%s", rr.Code, core.cancelID, rr.Body.String())
	}
}

func TestHandleRemindersIncludesPendingRowsOutsideRecentWindow(t *testing.T) {
	old := ipc.Reminder{ID: 1, Status: "pending", Payload: `{"text":"old but pending"}`}
	recent := make([]ipc.Reminder, 50)
	for i := range recent {
		recent[i] = ipc.Reminder{ID: int64(i + 2), Status: "fired", Payload: `{"text":"history"}`}
	}
	core := &reminderCore{pending: []ipc.Reminder{old}, reminders: recent}
	rr := httptest.NewRecorder()
	handleReminders(rr, httptest.NewRequest(http.MethodGet, "/reminders", nil), core)
	if rr.Code != http.StatusOK || !strings.Contains(rr.Body.String(), "old but pending") ||
		!strings.Contains(rr.Body.String(), `value="1"`) {
		t.Fatalf("old pending reminder is not reachable: status=%d body=%s", rr.Code, rr.Body.String())
	}
}

func TestHandleRemindersRejectsMalformedPosts(t *testing.T) {
	for _, tc := range []struct {
		name   string
		action string
		id     string
	}{
		{"unknown action", "delete", "23"},
		{"missing id", "cancel", ""},
		{"non-numeric id", "cancel", "twenty-three"},
		{"non-positive id", "cancel", "0"},
	} {
		t.Run(tc.name, func(t *testing.T) {
			core := &reminderCore{}
			rr := httptest.NewRecorder()
			handleReminders(rr, reminderPost(tc.action, tc.id), core)
			if rr.Code != http.StatusBadRequest {
				t.Fatalf("status = %d, want 400; body=%s", rr.Code, rr.Body.String())
			}
			if core.cancelID != 0 {
				t.Fatalf("CancelReminder called with %d for malformed post", core.cancelID)
			}
		})
	}
}

func TestHandleRemindersCancelErrors(t *testing.T) {
	for _, tc := range []struct {
		name   string
		err    error
		status int
		public string
	}{
		{"missing", ipc.ErrReminderNotFound, http.StatusNotFound, "reminder not found"},
		{"delivery started", ipc.ErrReminderInFlight, http.StatusConflict, "reminder delivery has already started"},
		{"terminal", ipc.ErrReminderState, http.StatusConflict, "reminder is no longer pending"},
		{"transport", errors.New("socket closed"), http.StatusBadGateway, "reminder cancellation failed"},
	} {
		t.Run(tc.name, func(t *testing.T) {
			core := &reminderCore{cancelErr: tc.err}
			rr := httptest.NewRecorder()
			handleReminders(rr, reminderPost("cancel", "23"), core)
			if rr.Code != tc.status || !strings.Contains(rr.Body.String(), tc.public) {
				t.Fatalf("status/body = %d %q, want %d containing %q", rr.Code, rr.Body.String(), tc.status, tc.public)
			}
			if got := rr.Header().Get("Content-Type"); !strings.HasPrefix(got, "application/problem+json") {
				t.Fatalf("content type = %q, want problem JSON", got)
			}
		})
	}
}

func TestHandleRemindersMethodAndListErrors(t *testing.T) {
	t.Run("method", func(t *testing.T) {
		rr := httptest.NewRecorder()
		handleReminders(rr, httptest.NewRequest(http.MethodDelete, "/reminders", nil), &reminderCore{})
		if rr.Code != http.StatusMethodNotAllowed {
			t.Fatalf("status = %d, want 405", rr.Code)
		}
	})
	t.Run("pending list", func(t *testing.T) {
		rr := httptest.NewRecorder()
		handleReminders(rr, httptest.NewRequest(http.MethodGet, "/reminders", nil), &reminderCore{pendingErr: errors.New("offline")})
		if rr.Code != http.StatusBadGateway || !strings.Contains(rr.Body.String(), "reminders unavailable") {
			t.Fatalf("status/body = %d %q, want 502 problem", rr.Code, rr.Body.String())
		}
	})
	t.Run("recent list", func(t *testing.T) {
		rr := httptest.NewRecorder()
		handleReminders(rr, httptest.NewRequest(http.MethodGet, "/reminders", nil), &reminderCore{listErr: errors.New("offline")})
		if rr.Code != http.StatusBadGateway || !strings.Contains(rr.Body.String(), "reminders unavailable") {
			t.Fatalf("status/body = %d %q, want 502 problem", rr.Code, rr.Body.String())
		}
	})
	t.Run("successful outcome remains explicit when redirected refresh fails", func(t *testing.T) {
		rr := httptest.NewRecorder()
		handleReminders(rr, httptest.NewRequest(http.MethodGet, "/reminders?cancelled=1", nil), &reminderCore{listErr: errors.New("offline")})
		if rr.Code != http.StatusBadGateway || !strings.Contains(rr.Body.String(), "reminder cancelled; refreshed list unavailable") {
			t.Fatalf("status/body = %d %q, want truthful refresh problem", rr.Code, rr.Body.String())
		}
	})
}
