// ABOUTME: Unit tests for the Settings page component.
// ABOUTME: Tests form rendering, data loading, validation, and save flow.
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";

// Mock next/navigation
const mockPush = vi.fn();
vi.mock("next/navigation", () => ({
  useRouter: () => ({
    push: mockPush,
  }),
}));

// 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
const mockFetch = vi.fn();
global.fetch = mockFetch;

import SettingsPage from "./page";

describe("SettingsPage", () => {
  const mockUser = {
    id: "user123",
    email: "test@example.com",
    cycleLength: 28,
    notificationTime: "08:00",
    timezone: "America/New_York",
    garminConnected: false,
    activeOverrides: [],
    lastPeriodDate: "2024-01-01",
    intensityGoalMenstrual: 75,
    intensityGoalFollicular: 150,
    intensityGoalOvulation: 100,
    intensityGoalEarlyLuteal: 120,
    intensityGoalLateLuteal: 50,
  };

  beforeEach(() => {
    vi.clearAllMocks();
    mockShowToast.success.mockClear();
    mockShowToast.error.mockClear();
    mockShowToast.info.mockClear();
    mockFetch.mockResolvedValue({
      ok: true,
      json: () => Promise.resolve(mockUser),
    });
  });

  describe("rendering", () => {
    it("renders the settings heading", async () => {
      render(<SettingsPage />);

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

    it("renders cycle length input", async () => {
      render(<SettingsPage />);

      await waitFor(() => {
        expect(screen.getByLabelText(/cycle length/i)).toBeInTheDocument();
      });
    });

    it("renders notification time input", async () => {
      render(<SettingsPage />);

      await waitFor(() => {
        expect(screen.getByLabelText(/notification time/i)).toBeInTheDocument();
      });
    });

    it("renders timezone input", async () => {
      render(<SettingsPage />);

      await waitFor(() => {
        expect(screen.getByLabelText(/timezone/i)).toBeInTheDocument();
      });
    });

    it("renders a save button", async () => {
      render(<SettingsPage />);

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

    it("renders a back link to dashboard", async () => {
      render(<SettingsPage />);

      await waitFor(() => {
        expect(screen.getByRole("link", { name: /back/i })).toHaveAttribute(
          "href",
          "/",
        );
      });
    });

    it("displays email as read-only", async () => {
      render(<SettingsPage />);

      await waitFor(() => {
        expect(screen.getByText("test@example.com")).toBeInTheDocument();
      });
    });

    it("renders Garmin connection section with manage link", async () => {
      render(<SettingsPage />);

      await waitFor(() => {
        expect(screen.getByText(/garmin connection/i)).toBeInTheDocument();
        expect(screen.getByRole("link", { name: /manage/i })).toHaveAttribute(
          "href",
          "/settings/garmin",
        );
      });
    });

    it("shows not connected when garminConnected is false", async () => {
      render(<SettingsPage />);

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

    it("shows connected when garminConnected is true", async () => {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        json: () => Promise.resolve({ ...mockUser, garminConnected: true }),
      });

      render(<SettingsPage />);

      await waitFor(() => {
        expect(screen.getByText(/connected to garmin/i)).toBeInTheDocument();
      });
    });
  });

  describe("data loading", () => {
    it("fetches user data on mount", async () => {
      render(<SettingsPage />);

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

    it("shows loading state while fetching", async () => {
      // Create a promise that we can control
      let resolveUser: (value: unknown) => void = () => {};
      const userPromise = new Promise((resolve) => {
        resolveUser = resolve;
      });
      mockFetch.mockReturnValue({
        ok: true,
        json: () => userPromise,
      });

      render(<SettingsPage />);

      expect(screen.getByText(/loading/i)).toBeInTheDocument();

      resolveUser(mockUser);

      await waitFor(() => {
        expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
      });
    });

    it("pre-fills form with current user values", async () => {
      render(<SettingsPage />);

      await waitFor(() => {
        expect(screen.getByLabelText(/cycle length/i)).toHaveValue(28);
        expect(screen.getByLabelText(/notification time/i)).toHaveValue(
          "08:00",
        );
        expect(screen.getByLabelText(/timezone/i)).toHaveValue(
          "America/New_York",
        );
      });
    });

    it("shows error if fetching fails", async () => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        json: () => Promise.resolve({ error: "Failed to fetch user" }),
      });

      render(<SettingsPage />);

      await waitFor(() => {
        expect(screen.getByRole("alert")).toBeInTheDocument();
        expect(screen.getByText(/failed to fetch user/i)).toBeInTheDocument();
      });
    });
  });

  describe("form submission", () => {
    it("calls PATCH /api/user with updated values on save", async () => {
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockUser),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve({ ...mockUser, cycleLength: 30 }),
        });

      render(<SettingsPage />);

      await waitFor(() => {
        expect(screen.getByLabelText(/cycle length/i)).toBeInTheDocument();
      });

      const cycleLengthInput = screen.getByLabelText(/cycle length/i);
      fireEvent.change(cycleLengthInput, { target: { value: "30" } });

      const saveButton = screen.getByRole("button", { name: /save/i });
      fireEvent.click(saveButton);

      await waitFor(() => {
        expect(mockFetch).toHaveBeenCalledWith("/api/user", {
          method: "PATCH",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            cycleLength: 30,
            notificationTime: "08:00",
            timezone: "America/New_York",
            intensityGoalMenstrual: 75,
            intensityGoalFollicular: 150,
            intensityGoalOvulation: 100,
            intensityGoalEarlyLuteal: 120,
            intensityGoalLateLuteal: 50,
          }),
        });
      });
    });

    it("shows loading state while saving", async () => {
      let resolveSave: (value: unknown) => void = () => {};
      const savePromise = new Promise((resolve) => {
        resolveSave = resolve;
      });

      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockUser),
        })
        .mockReturnValueOnce({
          ok: true,
          json: () => savePromise,
        });

      render(<SettingsPage />);

      await waitFor(() => {
        expect(screen.getByLabelText(/cycle length/i)).toBeInTheDocument();
      });

      const saveButton = screen.getByRole("button", { name: /save/i });
      fireEvent.click(saveButton);

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

      resolveSave(mockUser);
    });

    it("disables inputs while saving", async () => {
      let resolveSave: (value: unknown) => void = () => {};
      const savePromise = new Promise((resolve) => {
        resolveSave = resolve;
      });

      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockUser),
        })
        .mockReturnValueOnce({
          ok: true,
          json: () => savePromise,
        });

      render(<SettingsPage />);

      await waitFor(() => {
        expect(screen.getByLabelText(/cycle length/i)).toBeInTheDocument();
      });

      const cycleLengthInput = screen.getByLabelText(/cycle length/i);
      const saveButton = screen.getByRole("button", { name: /save/i });
      fireEvent.click(saveButton);

      await waitFor(() => {
        expect(cycleLengthInput).toBeDisabled();
        expect(screen.getByLabelText(/notification time/i)).toBeDisabled();
        expect(screen.getByLabelText(/timezone/i)).toBeDisabled();
        expect(screen.getByRole("button", { name: /saving/i })).toBeDisabled();
      });

      resolveSave(mockUser);
    });

    it("shows success toast on save", async () => {
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockUser),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve({ ...mockUser, cycleLength: 30 }),
        });

      render(<SettingsPage />);

      await waitFor(() => {
        expect(screen.getByLabelText(/cycle length/i)).toBeInTheDocument();
      });

      const saveButton = screen.getByRole("button", { name: /save/i });
      fireEvent.click(saveButton);

      await waitFor(() => {
        expect(mockShowToast.success).toHaveBeenCalledWith(
          "Settings saved successfully",
        );
      });
    });

    it("shows error toast on save failure", async () => {
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockUser),
        })
        .mockResolvedValueOnce({
          ok: false,
          json: () =>
            Promise.resolve({ error: "cycleLength must be between 21 and 45" }),
        });

      render(<SettingsPage />);

      await waitFor(() => {
        expect(screen.getByLabelText(/cycle length/i)).toBeInTheDocument();
      });

      const saveButton = screen.getByRole("button", { name: /save/i });
      fireEvent.click(saveButton);

      await waitFor(() => {
        expect(mockShowToast.error).toHaveBeenCalledWith(
          "cycleLength must be between 21 and 45",
        );
      });
    });

    it("re-enables form after error", async () => {
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockUser),
        })
        .mockResolvedValueOnce({
          ok: false,
          json: () => Promise.resolve({ error: "Failed to save" }),
        });

      render(<SettingsPage />);

      await waitFor(() => {
        expect(screen.getByLabelText(/cycle length/i)).toBeInTheDocument();
      });

      const saveButton = screen.getByRole("button", { name: /save/i });
      fireEvent.click(saveButton);

      await waitFor(() => {
        expect(mockShowToast.error).toHaveBeenCalled();
      });

      expect(screen.getByLabelText(/cycle length/i)).not.toBeDisabled();
      expect(screen.getByLabelText(/notification time/i)).not.toBeDisabled();
      expect(screen.getByLabelText(/timezone/i)).not.toBeDisabled();
      expect(screen.getByRole("button", { name: /save/i })).not.toBeDisabled();
    });
  });

  describe("validation", () => {
    it("validates cycle length minimum (21)", async () => {
      render(<SettingsPage />);

      await waitFor(() => {
        expect(screen.getByLabelText(/cycle length/i)).toBeInTheDocument();
      });

      const cycleLengthInput = screen.getByLabelText(/cycle length/i);
      expect(cycleLengthInput).toHaveAttribute("min", "21");
    });

    it("validates cycle length maximum (45)", async () => {
      render(<SettingsPage />);

      await waitFor(() => {
        expect(screen.getByLabelText(/cycle length/i)).toBeInTheDocument();
      });

      const cycleLengthInput = screen.getByLabelText(/cycle length/i);
      expect(cycleLengthInput).toHaveAttribute("max", "45");
    });

    it("has cycle length input as number type", async () => {
      render(<SettingsPage />);

      await waitFor(() => {
        expect(screen.getByLabelText(/cycle length/i)).toBeInTheDocument();
      });

      const cycleLengthInput = screen.getByLabelText(/cycle length/i);
      expect(cycleLengthInput).toHaveAttribute("type", "number");
    });

    it("has notification time input as time type", async () => {
      render(<SettingsPage />);

      await waitFor(() => {
        expect(screen.getByLabelText(/notification time/i)).toBeInTheDocument();
      });

      const notificationTimeInput = screen.getByLabelText(/notification time/i);
      expect(notificationTimeInput).toHaveAttribute("type", "time");
    });

    it("requires timezone to be non-empty", async () => {
      render(<SettingsPage />);

      await waitFor(() => {
        expect(screen.getByLabelText(/timezone/i)).toBeInTheDocument();
      });

      const timezoneInput = screen.getByLabelText(/timezone/i);
      expect(timezoneInput).toHaveAttribute("required");
    });
  });

  describe("toast notifications", () => {
    it("shows toast with fetch error on load failure", async () => {
      mockFetch.mockResolvedValueOnce({
        ok: false,
        json: () => Promise.resolve({ error: "Failed to fetch user" }),
      });

      render(<SettingsPage />);

      await waitFor(() => {
        expect(mockShowToast.error).toHaveBeenCalledWith(
          "Unable to fetch data. Retry?",
        );
      });
    });
  });

  describe("accessibility", () => {
    it("wraps content in a main element", async () => {
      render(<SettingsPage />);

      await waitFor(() => {
        expect(screen.getByRole("main")).toBeInTheDocument();
      });
    });

    it("has proper heading structure with h1", async () => {
      render(<SettingsPage />);

      await waitFor(() => {
        const heading = screen.getByRole("heading", { level: 1 });
        expect(heading).toBeInTheDocument();
        expect(heading).toHaveTextContent(/settings/i);
      });
    });
  });

  describe("logout", () => {
    it("renders a logout button", async () => {
      render(<SettingsPage />);

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

    it("calls POST /api/auth/logout when logout button clicked", async () => {
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockUser),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () =>
            Promise.resolve({
              success: true,
              message: "Logged out successfully",
              redirectTo: "/login",
            }),
        });

      render(<SettingsPage />);

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

      const logoutButton = screen.getByRole("button", { name: /log out/i });
      fireEvent.click(logoutButton);

      await waitFor(() => {
        expect(mockFetch).toHaveBeenCalledWith("/api/auth/logout", {
          method: "POST",
        });
      });
    });

    it("redirects to login page after logout", async () => {
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockUser),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () =>
            Promise.resolve({
              success: true,
              message: "Logged out successfully",
              redirectTo: "/login",
            }),
        });

      render(<SettingsPage />);

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

      const logoutButton = screen.getByRole("button", { name: /log out/i });
      fireEvent.click(logoutButton);

      await waitFor(() => {
        expect(mockPush).toHaveBeenCalledWith("/login");
      });
    });

    it("shows loading state while logging out", async () => {
      let resolveLogout: (value: unknown) => void = () => {};
      const logoutPromise = new Promise((resolve) => {
        resolveLogout = resolve;
      });

      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockUser),
        })
        .mockReturnValueOnce({
          ok: true,
          json: () => logoutPromise,
        });

      render(<SettingsPage />);

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

      const logoutButton = screen.getByRole("button", { name: /log out/i });
      fireEvent.click(logoutButton);

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

      resolveLogout({
        success: true,
        message: "Logged out successfully",
        redirectTo: "/login",
      });
    });

    it("shows error toast if logout fails", async () => {
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockUser),
        })
        .mockResolvedValueOnce({
          ok: false,
          json: () => Promise.resolve({ error: "Logout failed" }),
        });

      render(<SettingsPage />);

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

      const logoutButton = screen.getByRole("button", { name: /log out/i });
      fireEvent.click(logoutButton);

      await waitFor(() => {
        expect(mockShowToast.error).toHaveBeenCalledWith("Logout failed");
      });
    });
  });

  describe("intensity goals section", () => {
    it("renders Weekly Intensity Goals section heading", async () => {
      render(<SettingsPage />);

      await waitFor(() => {
        expect(
          screen.getByRole("heading", { name: /weekly intensity goals/i }),
        ).toBeInTheDocument();
      });
    });

    it("renders input for menstrual phase goal", async () => {
      render(<SettingsPage />);

      await waitFor(() => {
        expect(screen.getByLabelText(/menstrual/i)).toBeInTheDocument();
      });
    });

    it("renders input for follicular phase goal", async () => {
      render(<SettingsPage />);

      await waitFor(() => {
        expect(screen.getByLabelText(/follicular/i)).toBeInTheDocument();
      });
    });

    it("renders input for ovulation phase goal", async () => {
      render(<SettingsPage />);

      await waitFor(() => {
        expect(screen.getByLabelText(/ovulation/i)).toBeInTheDocument();
      });
    });

    it("renders input for early luteal phase goal", async () => {
      render(<SettingsPage />);

      await waitFor(() => {
        expect(screen.getByLabelText(/early luteal/i)).toBeInTheDocument();
      });
    });

    it("renders input for late luteal phase goal", async () => {
      render(<SettingsPage />);

      await waitFor(() => {
        expect(screen.getByLabelText(/late luteal/i)).toBeInTheDocument();
      });
    });

    it("pre-fills intensity goal inputs with current user values", async () => {
      render(<SettingsPage />);

      await waitFor(() => {
        expect(screen.getByLabelText(/menstrual/i)).toHaveValue(75);
        expect(screen.getByLabelText(/follicular/i)).toHaveValue(150);
        expect(screen.getByLabelText(/ovulation/i)).toHaveValue(100);
        expect(screen.getByLabelText(/early luteal/i)).toHaveValue(120);
        expect(screen.getByLabelText(/late luteal/i)).toHaveValue(50);
      });
    });

    it("includes intensity goals in PATCH request when saving", async () => {
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockUser),
        })
        .mockResolvedValueOnce({
          ok: true,
          json: () =>
            Promise.resolve({ ...mockUser, intensityGoalMenstrual: 80 }),
        });

      render(<SettingsPage />);

      await waitFor(() => {
        expect(screen.getByLabelText(/menstrual/i)).toBeInTheDocument();
      });

      const menstrualInput = screen.getByLabelText(/menstrual/i);
      fireEvent.change(menstrualInput, { target: { value: "80" } });

      const saveButton = screen.getByRole("button", { name: /save/i });
      fireEvent.click(saveButton);

      await waitFor(() => {
        expect(mockFetch).toHaveBeenCalledWith("/api/user", {
          method: "PATCH",
          headers: { "Content-Type": "application/json" },
          body: expect.stringContaining('"intensityGoalMenstrual":80'),
        });
      });
    });

    it("has number type for all intensity goal inputs", async () => {
      render(<SettingsPage />);

      await waitFor(() => {
        expect(screen.getByLabelText(/menstrual/i)).toHaveAttribute(
          "type",
          "number",
        );
        expect(screen.getByLabelText(/follicular/i)).toHaveAttribute(
          "type",
          "number",
        );
        expect(screen.getByLabelText(/ovulation/i)).toHaveAttribute(
          "type",
          "number",
        );
        expect(screen.getByLabelText(/early luteal/i)).toHaveAttribute(
          "type",
          "number",
        );
        expect(screen.getByLabelText(/late luteal/i)).toHaveAttribute(
          "type",
          "number",
        );
      });
    });

    it("validates minimum value of 0 for intensity goals", async () => {
      render(<SettingsPage />);

      await waitFor(() => {
        expect(screen.getByLabelText(/menstrual/i)).toHaveAttribute("min", "0");
      });
    });

    it("disables intensity goal inputs while saving", async () => {
      let resolveSave: (value: unknown) => void = () => {};
      const savePromise = new Promise((resolve) => {
        resolveSave = resolve;
      });

      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          json: () => Promise.resolve(mockUser),
        })
        .mockReturnValueOnce({
          ok: true,
          json: () => savePromise,
        });

      render(<SettingsPage />);

      await waitFor(() => {
        expect(screen.getByLabelText(/menstrual/i)).toBeInTheDocument();
      });

      const saveButton = screen.getByRole("button", { name: /save/i });
      fireEvent.click(saveButton);

      await waitFor(() => {
        expect(screen.getByLabelText(/menstrual/i)).toBeDisabled();
        expect(screen.getByLabelText(/follicular/i)).toBeDisabled();
        expect(screen.getByLabelText(/ovulation/i)).toBeDisabled();
        expect(screen.getByLabelText(/early luteal/i)).toBeDisabled();
        expect(screen.getByLabelText(/late luteal/i)).toBeDisabled();
      });

      resolveSave(mockUser);
    });
  });
});
