import { render, screen, waitFor, cleanup, fireEvent } from "@testing-library/react";
import { expect, test, vi, beforeEach, afterEach, describe } from "vitest";
import ProfilePage from "./page";

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

// Default mock values
let mockUser: { id: number; email: string; roles: string[]; permissions: string[] } | null = {
  id: 1,
  email: "test@example.com",
  roles: ["regular"],
  permissions: ["view_counter", "increment_counter", "use_sum"],
};
let mockIsLoading = false;
const mockLogout = vi.fn();
const mockHasRole = vi.fn((role: string) => mockUser?.roles.includes(role) ?? false);

vi.mock("../auth-context", () => ({
  useAuth: () => ({
    user: mockUser,
    isLoading: mockIsLoading,
    logout: mockLogout,
    hasRole: mockHasRole,
  }),
}));

// Mock profile data
const mockProfileData = {
  contact_email: "contact@example.com",
  telegram: "@testuser",
  signal: "signal.42",
  nostr_npub: "npub1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqzqujme",
};

beforeEach(() => {
  vi.clearAllMocks();
  // Reset to authenticated regular user
  mockUser = {
    id: 1,
    email: "test@example.com",
    roles: ["regular"],
    permissions: ["view_counter", "increment_counter", "use_sum"],
  };
  mockIsLoading = false;
  mockHasRole.mockImplementation((role: string) => mockUser?.roles.includes(role) ?? false);
});

afterEach(() => {
  cleanup();
});

describe("ProfilePage - Display", () => {
  test("renders loading state initially", () => {
    mockIsLoading = true;
    vi.spyOn(global, "fetch").mockImplementation(() => new Promise(() => {}));

    render(<ProfilePage />);
    expect(screen.getByText("Loading...")).toBeDefined();
  });

  test("renders profile page title", async () => {
    vi.spyOn(global, "fetch").mockResolvedValue({
      ok: true,
      json: () => Promise.resolve(mockProfileData),
    } as Response);

    render(<ProfilePage />);
    await waitFor(() => {
      expect(screen.getByRole("heading", { name: "My Profile" })).toBeDefined();
    });
  });

  test("displays login email as read-only", async () => {
    vi.spyOn(global, "fetch").mockResolvedValue({
      ok: true,
      json: () => Promise.resolve(mockProfileData),
    } as Response);

    render(<ProfilePage />);
    await waitFor(() => {
      const loginEmailInput = screen.getByDisplayValue("test@example.com");
      expect(loginEmailInput).toBeDefined();
      expect(loginEmailInput).toHaveProperty("disabled", true);
    });
  });

  test("shows read-only badge for login email", async () => {
    vi.spyOn(global, "fetch").mockResolvedValue({
      ok: true,
      json: () => Promise.resolve(mockProfileData),
    } as Response);

    render(<ProfilePage />);
    await waitFor(() => {
      expect(screen.getByText("Read only")).toBeDefined();
    });
  });

  test("shows hint about login email", async () => {
    vi.spyOn(global, "fetch").mockResolvedValue({
      ok: true,
      json: () => Promise.resolve(mockProfileData),
    } as Response);

    render(<ProfilePage />);
    await waitFor(() => {
      expect(screen.getByText(/cannot be changed/i)).toBeDefined();
    });
  });

  test("displays contact details section hint", async () => {
    vi.spyOn(global, "fetch").mockResolvedValue({
      ok: true,
      json: () => Promise.resolve(mockProfileData),
    } as Response);

    render(<ProfilePage />);
    await waitFor(() => {
      expect(screen.getByText(/communication purposes only/i)).toBeDefined();
    });
  });

  test("displays fetched profile data", async () => {
    vi.spyOn(global, "fetch").mockResolvedValue({
      ok: true,
      json: () => Promise.resolve(mockProfileData),
    } as Response);

    render(<ProfilePage />);
    await waitFor(() => {
      expect(screen.getByDisplayValue("contact@example.com")).toBeDefined();
      expect(screen.getByDisplayValue("@testuser")).toBeDefined();
      expect(screen.getByDisplayValue("signal.42")).toBeDefined();
      expect(screen.getByDisplayValue(mockProfileData.nostr_npub)).toBeDefined();
    });
  });

  test("fetches profile with credentials", async () => {
    const fetchSpy = vi.spyOn(global, "fetch").mockResolvedValue({
      ok: true,
      json: () => Promise.resolve(mockProfileData),
    } as Response);

    render(<ProfilePage />);

    await waitFor(() => {
      expect(fetchSpy).toHaveBeenCalledWith(
        "http://localhost:8000/api/profile",
        expect.objectContaining({
          credentials: "include",
        })
      );
    });
  });

  test("displays empty fields when profile has null values", async () => {
    vi.spyOn(global, "fetch").mockResolvedValue({
      ok: true,
      json: () =>
        Promise.resolve({
          contact_email: null,
          telegram: null,
          signal: null,
          nostr_npub: null,
        }),
    } as Response);

    render(<ProfilePage />);
    await waitFor(() => {
      // Check that inputs exist with empty values (placeholders shown)
      const telegramInput = document.getElementById("telegram") as HTMLInputElement;
      expect(telegramInput.value).toBe("");
    });
  });
});

describe("ProfilePage - Navigation", () => {
  test("shows nav links for regular user", async () => {
    vi.spyOn(global, "fetch").mockResolvedValue({
      ok: true,
      json: () => Promise.resolve(mockProfileData),
    } as Response);

    render(<ProfilePage />);
    await waitFor(() => {
      expect(screen.getByText("Counter")).toBeDefined();
      expect(screen.getByText("Sum")).toBeDefined();
    });
  });

  test("highlights My Profile in nav", async () => {
    vi.spyOn(global, "fetch").mockResolvedValue({
      ok: true,
      json: () => Promise.resolve(mockProfileData),
    } as Response);

    render(<ProfilePage />);
    await waitFor(() => {
      // My Profile should be visible (as current page indicator)
      const navItems = screen.getAllByText("My Profile");
      expect(navItems.length).toBeGreaterThan(0);
    });
  });
});

describe("ProfilePage - Access Control", () => {
  test("redirects to login when not authenticated", async () => {
    mockUser = null;

    render(<ProfilePage />);

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

  test("redirects admin to audit page", async () => {
    mockUser = {
      id: 1,
      email: "admin@example.com",
      roles: ["admin"],
      permissions: ["view_audit"],
    };

    render(<ProfilePage />);

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

  test("does not fetch profile for admin user", async () => {
    mockUser = {
      id: 1,
      email: "admin@example.com",
      roles: ["admin"],
      permissions: ["view_audit"],
    };
    const fetchSpy = vi.spyOn(global, "fetch");

    render(<ProfilePage />);

    // Give it a moment to potentially fetch
    await new Promise((r) => setTimeout(r, 100));
    expect(fetchSpy).not.toHaveBeenCalled();
  });
});

describe("ProfilePage - Loading State", () => {
  test("does not redirect while loading", () => {
    mockIsLoading = true;
    mockUser = null;

    render(<ProfilePage />);

    expect(mockPush).not.toHaveBeenCalled();
  });

  test("shows loading indicator while loading", () => {
    mockIsLoading = true;

    render(<ProfilePage />);

    expect(screen.getByText("Loading...")).toBeDefined();
  });
});

describe("ProfilePage - Form Behavior", () => {
  test("submit button is disabled when no changes", async () => {
    vi.spyOn(global, "fetch").mockResolvedValue({
      ok: true,
      json: () => Promise.resolve(mockProfileData),
    } as Response);

    render(<ProfilePage />);
    await waitFor(() => {
      const submitButton = screen.getByRole("button", { name: /save changes/i });
      expect(submitButton).toHaveProperty("disabled", true);
    });
  });

  test("submit button is enabled after field changes", async () => {
    vi.spyOn(global, "fetch").mockResolvedValue({
      ok: true,
      json: () => Promise.resolve(mockProfileData),
    } as Response);

    render(<ProfilePage />);

    await waitFor(() => {
      expect(screen.getByDisplayValue("@testuser")).toBeDefined();
    });

    const telegramInput = screen.getByDisplayValue("@testuser");
    fireEvent.change(telegramInput, { target: { value: "@newhandle" } });

    await waitFor(() => {
      const submitButton = screen.getByRole("button", { name: /save changes/i });
      expect(submitButton).toHaveProperty("disabled", false);
    });
  });

  test("auto-prepends @ to telegram when user starts with letter", async () => {
    vi.spyOn(global, "fetch").mockResolvedValue({
      ok: true,
      json: () =>
        Promise.resolve({
          contact_email: null,
          telegram: null,
          signal: null,
          nostr_npub: null,
        }),
    } as Response);

    render(<ProfilePage />);

    await waitFor(() => {
      expect(screen.getByRole("heading", { name: "My Profile" })).toBeDefined();
    });

    const telegramInput = document.getElementById("telegram") as HTMLInputElement;

    // Type a letter without @ - should auto-prepend @
    fireEvent.change(telegramInput, { target: { value: "myhandle" } });

    expect(telegramInput.value).toBe("@myhandle");
  });

  test("does not auto-prepend @ if user types @ first", async () => {
    vi.spyOn(global, "fetch").mockResolvedValue({
      ok: true,
      json: () =>
        Promise.resolve({
          contact_email: null,
          telegram: null,
          signal: null,
          nostr_npub: null,
        }),
    } as Response);

    render(<ProfilePage />);

    await waitFor(() => {
      expect(screen.getByRole("heading", { name: "My Profile" })).toBeDefined();
    });

    const telegramInput = document.getElementById("telegram") as HTMLInputElement;

    // User types @ first - no auto-prepend
    fireEvent.change(telegramInput, { target: { value: "@myhandle" } });

    expect(telegramInput.value).toBe("@myhandle");
  });
});

describe("ProfilePage - Form Submission", () => {
  test("shows success toast after successful save", async () => {
    const fetchSpy = vi
      .spyOn(global, "fetch")
      .mockResolvedValueOnce({
        ok: true,
        json: () =>
          Promise.resolve({
            contact_email: null,
            telegram: null,
            signal: null,
            nostr_npub: null,
          }),
      } as Response)
      .mockResolvedValueOnce({
        ok: true,
        json: () =>
          Promise.resolve({
            contact_email: "new@example.com",
            telegram: null,
            signal: null,
            nostr_npub: null,
          }),
      } as Response);

    render(<ProfilePage />);

    await waitFor(() => {
      expect(screen.getByRole("heading", { name: "My Profile" })).toBeDefined();
    });

    // Change email
    const emailInput = document.getElementById("contact_email") as HTMLInputElement;
    fireEvent.change(emailInput, { target: { value: "new@example.com" } });

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

    await waitFor(() => {
      expect(screen.getByText(/saved successfully/i)).toBeDefined();
    });

    // Verify PUT was called
    expect(fetchSpy).toHaveBeenCalledWith(
      "http://localhost:8000/api/profile",
      expect.objectContaining({
        method: "PUT",
        credentials: "include",
      })
    );
  });

  test("shows inline errors from backend validation", async () => {
    vi.spyOn(global, "fetch")
      .mockResolvedValueOnce({
        ok: true,
        json: () =>
          Promise.resolve({
            contact_email: null,
            telegram: null,
            signal: null,
            nostr_npub: null,
          }),
      } as Response)
      .mockResolvedValueOnce({
        ok: false,
        status: 422,
        json: () =>
          Promise.resolve({
            detail: {
              field_errors: {
                telegram: "Backend error: invalid handle",
              },
            },
          }),
      } as Response);

    render(<ProfilePage />);

    await waitFor(() => {
      expect(screen.getByRole("heading", { name: "My Profile" })).toBeDefined();
    });

    // Enter a value that passes frontend validation but fails backend
    const telegramInput = document.getElementById("telegram") as HTMLInputElement;
    fireEvent.change(telegramInput, { target: { value: "@validfrontend" } });

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

    await waitFor(() => {
      expect(screen.getByText(/backend error/i)).toBeDefined();
    });
  });

  test("shows error toast on network failure", async () => {
    vi.spyOn(global, "fetch")
      .mockResolvedValueOnce({
        ok: true,
        json: () =>
          Promise.resolve({
            contact_email: null,
            telegram: null,
            signal: null,
            nostr_npub: null,
          }),
      } as Response)
      .mockRejectedValueOnce(new Error("Network error"));

    render(<ProfilePage />);

    await waitFor(() => {
      expect(screen.getByRole("heading", { name: "My Profile" })).toBeDefined();
    });

    // Change something
    const emailInput = document.getElementById("contact_email") as HTMLInputElement;
    fireEvent.change(emailInput, { target: { value: "new@example.com" } });

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

    await waitFor(() => {
      expect(screen.getByText(/network error/i)).toBeDefined();
    });
  });

  test("submit button shows 'Saving...' while submitting", async () => {
    let resolveSubmit: (value: Response) => void;
    const submitPromise = new Promise<Response>((resolve) => {
      resolveSubmit = resolve;
    });

    vi.spyOn(global, "fetch")
      .mockResolvedValueOnce({
        ok: true,
        json: () =>
          Promise.resolve({
            contact_email: null,
            telegram: null,
            signal: null,
            nostr_npub: null,
          }),
      } as Response)
      .mockReturnValueOnce(submitPromise as Promise<Response>);

    render(<ProfilePage />);

    await waitFor(() => {
      expect(screen.getByRole("heading", { name: "My Profile" })).toBeDefined();
    });

    // Change something
    const emailInput = document.getElementById("contact_email") as HTMLInputElement;
    fireEvent.change(emailInput, { target: { value: "new@example.com" } });

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

    await waitFor(() => {
      expect(screen.getByText("Saving...")).toBeDefined();
    });

    // Resolve the promise
    resolveSubmit!({
      ok: true,
      json: () =>
        Promise.resolve({
          contact_email: "new@example.com",
          telegram: null,
          signal: null,
          nostr_npub: null,
        }),
    } as Response);

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