// ABOUTME: Unit tests for the Dashboard page component.
// ABOUTME: Tests data fetching, component rendering, override toggles, and error handling.
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";

// Mock showToast utility with vi.hoisted to avoid hoisting issues
const mockShowToast = vi.hoisted(() => ({
  success: vi.fn(),
  error: vi.fn(),
  info: vi.fn(),
}));
vi.mock("@/components/ui/toaster", () => ({
  showToast: mockShowToast,
}));

// Mock fetch globally
const mockFetch = vi.fn();
global.fetch = mockFetch;

import Dashboard from "./page";

// Mock response data matching /api/today shape
const mockTodayResponse = {
  decision: {
    status: "TRAIN",
    reason: "All systems go! Body battery and HRV look good.",
    icon: "💪",
  },
  cycleDay: 12,
  phase: "FOLLICULAR",
  phaseConfig: {
    name: "FOLLICULAR",
    days: [6, 13],
    weeklyLimit: 300,
    dailyAvg: 43,
    trainingType: "Build strength and endurance",
  },
  daysUntilNextPhase: 2,
  cycleLength: 31,
  biometrics: {
    hrvStatus: "Balanced",
    bodyBatteryCurrent: 75,
    bodyBatteryYesterdayLow: 45,
    weekIntensityMinutes: 120,
    phaseLimit: 300,
  },
  nutrition: {
    seeds: "Flax & Pumpkin seeds",
    carbRange: "100-150g",
    ketoGuidance: "Optional",
  },
};

const mockUserResponse = {
  id: "user123",
  email: "test@example.com",
  activeOverrides: [],
  cycleLength: 31,
  lastPeriodDate: "2024-01-01",
  garminConnected: true,
  notificationTime: "07:00",
  timezone: "America/New_York",
};

describe("Dashboard", () => {
  beforeEach(() => {
    vi.clearAllMocks();
    mockShowToast.success.mockClear();
    mockShowToast.error.mockClear();
    mockShowToast.info.mockClear();
  });

  describe("rendering", () => {
    it("renders the PhaseFlow header", async () => {
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockTodayResponse),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockUserResponse),
        });

      render(<Dashboard />);

      expect(screen.getByText("PhaseFlow")).toBeInTheDocument();
    });

    it("renders the settings link", async () => {
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockTodayResponse),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockUserResponse),
        });

      render(<Dashboard />);

      const settingsLink = screen.getByRole("link", { name: /settings/i });
      expect(settingsLink).toBeInTheDocument();
      expect(settingsLink).toHaveAttribute("href", "/settings");
    });

    it("shows loading state initially", () => {
      mockFetch.mockImplementation(() => new Promise(() => {})); // Never resolves

      render(<Dashboard />);

      // Check for skeleton components which have aria-label "Loading ..."
      expect(
        screen.getByRole("region", { name: /loading decision/i }),
      ).toBeInTheDocument();
    });
  });

  describe("data fetching", () => {
    it("fetches data from /api/today on mount", async () => {
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockTodayResponse),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockUserResponse),
        });

      render(<Dashboard />);

      await waitFor(() => {
        expect(mockFetch).toHaveBeenCalledWith("/api/today");
      });
    });

    it("fetches user data from /api/user on mount", async () => {
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockTodayResponse),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockUserResponse),
        });

      render(<Dashboard />);

      await waitFor(() => {
        expect(mockFetch).toHaveBeenCalledWith("/api/user");
      });
    });
  });

  describe("DecisionCard", () => {
    it("displays the decision status", async () => {
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockTodayResponse),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockUserResponse),
        });

      render(<Dashboard />);

      await waitFor(() => {
        expect(screen.getByText("TRAIN")).toBeInTheDocument();
      });
    });

    it("displays the decision icon", async () => {
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockTodayResponse),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockUserResponse),
        });

      render(<Dashboard />);

      await waitFor(() => {
        expect(screen.getByText("💪")).toBeInTheDocument();
      });
    });

    it("displays the decision reason", async () => {
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockTodayResponse),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockUserResponse),
        });

      render(<Dashboard />);

      await waitFor(() => {
        expect(screen.getByText(/all systems go/i)).toBeInTheDocument();
      });
    });
  });

  describe("DataPanel", () => {
    it("displays body battery current", async () => {
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockTodayResponse),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockUserResponse),
        });

      render(<Dashboard />);

      await waitFor(() => {
        expect(screen.getByText(/body battery.*75/i)).toBeInTheDocument();
      });
    });

    it("displays HRV status", async () => {
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockTodayResponse),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockUserResponse),
        });

      render(<Dashboard />);

      await waitFor(() => {
        expect(screen.getByTestId("hrv-status")).toHaveTextContent("Balanced");
      });
    });

    it("displays week intensity and phase limit", async () => {
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockTodayResponse),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockUserResponse),
        });

      render(<Dashboard />);

      await waitFor(() => {
        expect(screen.getByText(/120\/300 min/i)).toBeInTheDocument();
      });
    });
  });

  describe("NutritionPanel", () => {
    it("displays seed guidance", async () => {
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockTodayResponse),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockUserResponse),
        });

      render(<Dashboard />);

      await waitFor(() => {
        expect(screen.getByText(/flax.*pumpkin/i)).toBeInTheDocument();
      });
    });

    it("displays carb range", async () => {
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockTodayResponse),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockUserResponse),
        });

      render(<Dashboard />);

      await waitFor(() => {
        expect(screen.getByText(/100-150g/i)).toBeInTheDocument();
      });
    });

    it("displays keto guidance", async () => {
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockTodayResponse),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockUserResponse),
        });

      render(<Dashboard />);

      await waitFor(() => {
        expect(screen.getByText(/keto.*optional/i)).toBeInTheDocument();
      });
    });
  });

  describe("OverrideToggles", () => {
    it("displays all override options", async () => {
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockTodayResponse),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockUserResponse),
        });

      render(<Dashboard />);

      await waitFor(() => {
        expect(screen.getByText("Flare Mode")).toBeInTheDocument();
        expect(screen.getByText("High Stress")).toBeInTheDocument();
        expect(screen.getByText("Poor Sleep")).toBeInTheDocument();
        expect(screen.getByText("PMS Symptoms")).toBeInTheDocument();
      });
    });

    it("shows active overrides as checked", async () => {
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockTodayResponse),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () =>
            Promise.resolve({
              ...mockUserResponse,
              activeOverrides: ["flare", "sleep"],
            }),
        });

      render(<Dashboard />);

      await waitFor(() => {
        const flareCheckbox = screen.getByRole("checkbox", {
          name: /flare mode/i,
        });
        const sleepCheckbox = screen.getByRole("checkbox", {
          name: /poor sleep/i,
        });
        const stressCheckbox = screen.getByRole("checkbox", {
          name: /high stress/i,
        });

        expect(flareCheckbox).toBeChecked();
        expect(sleepCheckbox).toBeChecked();
        expect(stressCheckbox).not.toBeChecked();
      });
    });

    it("calls POST /api/overrides when toggling on an override", async () => {
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockTodayResponse),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockUserResponse),
        });

      render(<Dashboard />);

      await waitFor(() => {
        expect(screen.getByText("Flare Mode")).toBeInTheDocument();
      });

      // Clear mock to track new calls
      mockFetch.mockClear();
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve({ activeOverrides: ["flare"] }),
      });

      const flareCheckbox = screen.getByRole("checkbox", {
        name: /flare mode/i,
      });
      fireEvent.click(flareCheckbox);

      await waitFor(() => {
        expect(mockFetch).toHaveBeenCalledWith("/api/overrides", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ override: "flare" }),
        });
      });
    });

    it("calls DELETE /api/overrides when toggling off an override", async () => {
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockTodayResponse),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () =>
            Promise.resolve({
              ...mockUserResponse,
              activeOverrides: ["flare"],
            }),
        });

      render(<Dashboard />);

      await waitFor(() => {
        const flareCheckbox = screen.getByRole("checkbox", {
          name: /flare mode/i,
        });
        expect(flareCheckbox).toBeChecked();
      });

      // Clear mock to track new calls
      mockFetch.mockClear();
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve({ activeOverrides: [] }),
      });

      const flareCheckbox = screen.getByRole("checkbox", {
        name: /flare mode/i,
      });
      fireEvent.click(flareCheckbox);

      await waitFor(() => {
        expect(mockFetch).toHaveBeenCalledWith("/api/overrides", {
          method: "DELETE",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ override: "flare" }),
        });
      });
    });

    it("refetches today data after toggle to update decision", async () => {
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockTodayResponse),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockUserResponse),
        });

      render(<Dashboard />);

      await waitFor(() => {
        expect(screen.getByText("Flare Mode")).toBeInTheDocument();
      });

      // Clear mock and set up for toggle + refetch
      mockFetch.mockClear();
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve({ activeOverrides: ["flare"] }),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () =>
            Promise.resolve({
              ...mockTodayResponse,
              decision: {
                status: "REST",
                reason: "Flare mode active - rest is essential.",
                icon: "🔥",
              },
            }),
        });

      const flareCheckbox = screen.getByRole("checkbox", {
        name: /flare mode/i,
      });
      fireEvent.click(flareCheckbox);

      await waitFor(() => {
        expect(screen.getByText("REST")).toBeInTheDocument();
        expect(screen.getByText(/flare mode active/i)).toBeInTheDocument();
      });
    });

    it("shows error toast when toggle fails", async () => {
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockTodayResponse),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockUserResponse),
        });

      render(<Dashboard />);

      await waitFor(() => {
        expect(screen.getByText("Flare Mode")).toBeInTheDocument();
      });

      // Clear mock and set up for failed toggle
      mockFetch.mockClear();
      mockFetch.mockResolvedValueOnce({
        ok: false,
        json: () => Promise.resolve({ error: "Failed to update override" }),
      });

      const flareCheckbox = screen.getByRole("checkbox", {
        name: /flare mode/i,
      });
      fireEvent.click(flareCheckbox);

      await waitFor(() => {
        expect(mockShowToast.error).toHaveBeenCalledWith(
          "Failed to update override",
        );
      });
    });
  });

  describe("error handling", () => {
    it("shows error message when /api/today fails", async () => {
      mockFetch
        .mockResolvedValueOnce({
          ok: false,
          status: 500,
          json: () => Promise.resolve({ error: "Internal server error" }),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockUserResponse),
        });

      render(<Dashboard />);

      await waitFor(() => {
        expect(screen.getByRole("alert")).toBeInTheDocument();
        expect(screen.getByText(/error/i)).toBeInTheDocument();
      });
    });

    it("shows setup message when user has no lastPeriodDate", async () => {
      mockFetch
        .mockResolvedValueOnce({
          ok: false,
          status: 400,
          json: () =>
            Promise.resolve({
              error:
                "User has no lastPeriodDate set. Please log your period start date first.",
            }),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () =>
            Promise.resolve({
              ...mockUserResponse,
              lastPeriodDate: null,
            }),
        });

      render(<Dashboard />);

      await waitFor(() => {
        // Multiple alerts may be present (error alert + onboarding banner)
        const alerts = screen.getAllByRole("alert");
        expect(alerts.length).toBeGreaterThan(0);
        // Check for the specific help text about getting started
        expect(
          screen.getByText(/please log your period start date to get started/i),
        ).toBeInTheDocument();
      });
    });
  });

  describe("cycle information", () => {
    it("displays current cycle day", async () => {
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockTodayResponse),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockUserResponse),
        });

      render(<Dashboard />);

      await waitFor(() => {
        expect(screen.getByText(/day 12/i)).toBeInTheDocument();
      });
    });

    it("displays current phase", async () => {
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockTodayResponse),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockUserResponse),
        });

      render(<Dashboard />);

      await waitFor(() => {
        // Check for phase in the cycle info header (uppercase, with Day X prefix)
        expect(screen.getByText(/Day 12 · FOLLICULAR/)).toBeInTheDocument();
      });
    });
  });

  describe("onboarding banners", () => {
    it("shows no onboarding banners when setup is complete", async () => {
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockTodayResponse),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockUserResponse),
        });

      render(<Dashboard />);

      await waitFor(() => {
        expect(screen.getByText("TRAIN")).toBeInTheDocument();
      });

      // Should not have any onboarding messages
      expect(
        screen.queryByText(/Connect your Garmin to get started/i),
      ).not.toBeInTheDocument();
      expect(
        screen.queryByText(/Set your last period date/i),
      ).not.toBeInTheDocument();
      expect(
        screen.queryByText(/Set your preferred notification time/i),
      ).not.toBeInTheDocument();
    });

    it("shows Garmin banner when not connected", async () => {
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockTodayResponse),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () =>
            Promise.resolve({
              ...mockUserResponse,
              garminConnected: false,
            }),
        });

      render(<Dashboard />);

      await waitFor(() => {
        expect(
          screen.getByText(/Connect your Garmin to get started/i),
        ).toBeInTheDocument();
      });

      // Verify the link points to Garmin settings
      const link = screen.getByRole("link", { name: /Connect/i });
      expect(link).toHaveAttribute("href", "/settings/garmin");
    });

    it("shows notification time banner when not set", async () => {
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockTodayResponse),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () =>
            Promise.resolve({
              ...mockUserResponse,
              notificationTime: "",
            }),
        });

      render(<Dashboard />);

      await waitFor(() => {
        expect(
          screen.getByText(/Set your preferred notification time/i),
        ).toBeInTheDocument();
      });

      // Verify the link points to settings
      const link = screen.getByRole("link", { name: /Configure/i });
      expect(link).toHaveAttribute("href", "/settings");
    });

    it("shows multiple banners when multiple items need setup", async () => {
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockTodayResponse),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () =>
            Promise.resolve({
              ...mockUserResponse,
              garminConnected: false,
              notificationTime: "",
            }),
        });

      render(<Dashboard />);

      await waitFor(() => {
        expect(
          screen.getByText(/Connect your Garmin to get started/i),
        ).toBeInTheDocument();
        expect(
          screen.getByText(/Set your preferred notification time/i),
        ).toBeInTheDocument();
      });
    });

    it("shows period date banner with action button", async () => {
      // Note: When lastPeriodDate is null, /api/today returns 400 error
      // But we still want to show the onboarding banner if userData shows no period
      // This test checks that the banner appears when userData indicates no period
      mockFetch
        .mockResolvedValueOnce({
          ok: false,
          status: 400,
          json: () =>
            Promise.resolve({
              error:
                "User has no lastPeriodDate set. Please log your period start date first.",
            }),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () =>
            Promise.resolve({
              ...mockUserResponse,
              lastPeriodDate: null,
            }),
        });

      render(<Dashboard />);

      // The error state handles this case with a specific message
      await waitFor(() => {
        expect(
          screen.getByText(/please log your period start date to get started/i),
        ).toBeInTheDocument();
      });
    });
  });

  describe("period date modal flow", () => {
    it("shows onboarding banner when todayData fails but userData shows no lastPeriodDate", async () => {
      mockFetch
        .mockResolvedValueOnce({
          ok: false,
          status: 400,
          json: () =>
            Promise.resolve({
              error:
                "User has no lastPeriodDate set. Please log your period start date first.",
            }),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () =>
            Promise.resolve({
              ...mockUserResponse,
              lastPeriodDate: null,
            }),
        });

      render(<Dashboard />);

      await waitFor(() => {
        expect(
          screen.getByText(/Set your last period date for accurate tracking/i),
        ).toBeInTheDocument();
        expect(
          screen.getByRole("button", { name: /set date/i }),
        ).toBeInTheDocument();
      });
    });

    it("opens period date modal when clicking Set date button", async () => {
      mockFetch
        .mockResolvedValueOnce({
          ok: false,
          status: 400,
          json: () =>
            Promise.resolve({
              error:
                "User has no lastPeriodDate set. Please log your period start date first.",
            }),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () =>
            Promise.resolve({
              ...mockUserResponse,
              lastPeriodDate: null,
            }),
        });

      render(<Dashboard />);

      await waitFor(() => {
        expect(
          screen.getByRole("button", { name: /set date/i }),
        ).toBeInTheDocument();
      });

      fireEvent.click(screen.getByRole("button", { name: /set date/i }));

      await waitFor(() => {
        expect(
          screen.getByRole("dialog", { name: /set period date/i }),
        ).toBeInTheDocument();
      });
    });

    it("calls POST /api/cycle/period when submitting date in modal", async () => {
      mockFetch
        .mockResolvedValueOnce({
          ok: false,
          status: 400,
          json: () =>
            Promise.resolve({
              error:
                "User has no lastPeriodDate set. Please log your period start date first.",
            }),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () =>
            Promise.resolve({
              ...mockUserResponse,
              lastPeriodDate: null,
            }),
        });

      render(<Dashboard />);

      await waitFor(() => {
        expect(
          screen.getByRole("button", { name: /set date/i }),
        ).toBeInTheDocument();
      });

      fireEvent.click(screen.getByRole("button", { name: /set date/i }));

      await waitFor(() => {
        expect(
          screen.getByRole("dialog", { name: /set period date/i }),
        ).toBeInTheDocument();
      });

      // Set up mock for the period POST and subsequent refetch
      mockFetch.mockClear();
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () =>
            Promise.resolve({
              message: "Period start date logged successfully",
              lastPeriodDate: "2024-01-15",
              cycleDay: 1,
              phase: "MENSTRUAL",
            }),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockTodayResponse),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () =>
            Promise.resolve({
              ...mockUserResponse,
              lastPeriodDate: "2024-01-15",
            }),
        });

      const dateInput = screen.getByLabelText(
        /when did your last period start/i,
      );
      fireEvent.change(dateInput, { target: { value: "2024-01-15" } });
      fireEvent.click(screen.getByRole("button", { name: /save/i }));

      await waitFor(() => {
        expect(mockFetch).toHaveBeenCalledWith("/api/cycle/period", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ startDate: "2024-01-15" }),
        });
      });
    });

    it("closes modal and refetches data after successful submission", async () => {
      mockFetch
        .mockResolvedValueOnce({
          ok: false,
          status: 400,
          json: () =>
            Promise.resolve({
              error:
                "User has no lastPeriodDate set. Please log your period start date first.",
            }),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () =>
            Promise.resolve({
              ...mockUserResponse,
              lastPeriodDate: null,
            }),
        });

      render(<Dashboard />);

      await waitFor(() => {
        expect(
          screen.getByRole("button", { name: /set date/i }),
        ).toBeInTheDocument();
      });

      fireEvent.click(screen.getByRole("button", { name: /set date/i }));

      await waitFor(() => {
        expect(
          screen.getByRole("dialog", { name: /set period date/i }),
        ).toBeInTheDocument();
      });

      // Set up mock for the period POST and successful refetch
      mockFetch.mockClear();
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () =>
            Promise.resolve({
              message: "Period start date logged successfully",
              lastPeriodDate: "2024-01-15",
              cycleDay: 1,
              phase: "MENSTRUAL",
            }),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockTodayResponse),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () =>
            Promise.resolve({
              ...mockUserResponse,
              lastPeriodDate: "2024-01-15",
            }),
        });

      const dateInput = screen.getByLabelText(
        /when did your last period start/i,
      );
      fireEvent.change(dateInput, { target: { value: "2024-01-15" } });
      fireEvent.click(screen.getByRole("button", { name: /save/i }));

      // Modal should close and dashboard should show normal content
      await waitFor(() => {
        expect(
          screen.queryByRole("dialog", { name: /set period date/i }),
        ).not.toBeInTheDocument();
        // Dashboard should now show the decision card
        expect(screen.getByText("TRAIN")).toBeInTheDocument();
      });
    });

    it("shows error in modal when API call fails", async () => {
      mockFetch
        .mockResolvedValueOnce({
          ok: false,
          status: 400,
          json: () =>
            Promise.resolve({
              error:
                "User has no lastPeriodDate set. Please log your period start date first.",
            }),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () =>
            Promise.resolve({
              ...mockUserResponse,
              lastPeriodDate: null,
            }),
        });

      render(<Dashboard />);

      await waitFor(() => {
        expect(
          screen.getByRole("button", { name: /set date/i }),
        ).toBeInTheDocument();
      });

      fireEvent.click(screen.getByRole("button", { name: /set date/i }));

      await waitFor(() => {
        expect(
          screen.getByRole("dialog", { name: /set period date/i }),
        ).toBeInTheDocument();
      });

      // Set up mock for failed API call
      mockFetch.mockClear();
      mockFetch.mockResolvedValueOnce({
        ok: false,
        status: 500,
        json: () => Promise.resolve({ error: "Failed to update period date" }),
      });

      const dateInput = screen.getByLabelText(
        /when did your last period start/i,
      );
      fireEvent.change(dateInput, { target: { value: "2024-01-15" } });
      fireEvent.click(screen.getByRole("button", { name: /save/i }));

      // Error should appear in modal (there may be multiple alerts - dashboard error + modal error)
      await waitFor(() => {
        const alerts = screen.getAllByRole("alert");
        const modalError = alerts.find((alert) =>
          alert.textContent?.includes("Failed to update period date"),
        );
        expect(modalError).toBeInTheDocument();
      });

      // Modal should still be open
      expect(
        screen.getByRole("dialog", { name: /set period date/i }),
      ).toBeInTheDocument();
    });
  });
});
