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: [
    "create_exchange",
    "view_own_exchanges",
    "cancel_own_exchange",
    "manage_own_profile",
  ],
};
let mockIsLoading = false;
const mockLogout = vi.fn();
const mockHasRole = vi.fn((role: string) => mockUser?.roles.includes(role) ?? false);
const mockHasPermission = vi.fn(
  (permission: string) => mockUser?.permissions.includes(permission) ?? false
);

vi.mock("../auth-context", () => ({
  useAuth: () => ({
    user: mockUser,
    isLoading: mockIsLoading,
    logout: mockLogout,
    hasRole: mockHasRole,
    hasPermission: mockHasPermission,
  }),
  Permission: {
    VIEW_AUDIT: "view_audit",
    FETCH_PRICE: "fetch_price",
    MANAGE_OWN_PROFILE: "manage_own_profile",
    MANAGE_INVITES: "manage_invites",
    VIEW_OWN_INVITES: "view_own_invites",
    CREATE_EXCHANGE: "create_exchange",
    VIEW_OWN_EXCHANGES: "view_own_exchanges",
    CANCEL_OWN_EXCHANGE: "cancel_own_exchange",
    MANAGE_AVAILABILITY: "manage_availability",
    VIEW_ALL_EXCHANGES: "view_all_exchanges",
    CANCEL_ANY_EXCHANGE: "cancel_any_exchange",
    COMPLETE_EXCHANGE: "complete_exchange",
  },
}));

// Mock profileApi
const mockGetProfile = vi.fn();
const mockUpdateProfile = vi.fn();

vi.mock("../api", async (importOriginal) => {
  const actual = await importOriginal<typeof import("../api")>();
  // Create a getter that returns the mock functions
  return {
    ...actual,
    get profileApi() {
      return {
        getProfile: mockGetProfile,
        updateProfile: mockUpdateProfile,
      };
    },
  };
});

// 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: [
      "create_exchange",
      "view_own_exchanges",
      "cancel_own_exchange",
      "manage_own_profile",
    ],
  };
  mockIsLoading = false;
  mockHasRole.mockImplementation((role: string) => mockUser?.roles.includes(role) ?? false);
  mockHasPermission.mockImplementation(
    (permission: string) => mockUser?.permissions.includes(permission) ?? false
  );
  // Reset API mocks
  mockGetProfile.mockResolvedValue(mockProfileData);
  mockUpdateProfile.mockResolvedValue(mockProfileData);
});

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

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

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

  test("renders profile page title", async () => {
    mockGetProfile.mockResolvedValue(mockProfileData);

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

  test("displays login email as read-only", async () => {
    mockGetProfile.mockResolvedValue(mockProfileData);

    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 () => {
    mockGetProfile.mockResolvedValue(mockProfileData);

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

  test("shows hint about login email", async () => {
    mockGetProfile.mockResolvedValue(mockProfileData);

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

  test("displays contact details section hint", async () => {
    mockGetProfile.mockResolvedValue(mockProfileData);

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

  test("displays fetched profile data", async () => {
    mockGetProfile.mockResolvedValue(mockProfileData);

    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 () => {
    mockGetProfile.mockResolvedValue(mockProfileData);

    render(<ProfilePage />);

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

  test("displays empty fields when profile has null values", async () => {
    mockGetProfile.mockResolvedValue({
      contact_email: null,
      telegram: null,
      signal: null,
      nostr_npub: null,
    });

    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 () => {
    mockGetProfile.mockResolvedValue(mockProfileData);

    render(<ProfilePage />);
    await waitFor(() => {
      expect(screen.getByText("Exchange")).toBeDefined();
      expect(screen.getByText("My Trades")).toBeDefined();
    });
  });

  test("highlights My Profile in nav", async () => {
    mockGetProfile.mockResolvedValue(mockProfileData);

    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 admin trades page", async () => {
    mockUser = {
      id: 1,
      email: "admin@example.com",
      roles: ["admin"],
      permissions: ["view_all_exchanges"],
    };

    render(<ProfilePage />);

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

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

    render(<ProfilePage />);

    // Give it a moment to potentially fetch
    await new Promise((r) => setTimeout(r, 100));
    expect(mockGetProfile).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 () => {
    mockGetProfile.mockResolvedValue(mockProfileData);

    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 () => {
    mockGetProfile.mockResolvedValue(mockProfileData);

    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 () => {
    mockGetProfile.mockResolvedValue({
      contact_email: null,
      telegram: null,
      signal: null,
      nostr_npub: null,
    });

    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 () => {
    mockGetProfile.mockResolvedValue({
      contact_email: null,
      telegram: null,
      signal: null,
      nostr_npub: null,
    });

    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 () => {
    mockGetProfile
      .mockResolvedValueOnce({
        contact_email: null,
        telegram: null,
        signal: null,
        nostr_npub: null,
      })
      .mockResolvedValueOnce({
        contact_email: "new@example.com",
        telegram: null,
        signal: null,
        nostr_npub: null,
      });
    mockUpdateProfile.mockResolvedValue({
      contact_email: "new@example.com",
      telegram: null,
      signal: null,
      nostr_npub: null,
    });

    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 updateProfile was called
    expect(mockUpdateProfile).toHaveBeenCalledWith({
      contact_email: "new@example.com",
      telegram: null,
      signal: null,
      nostr_npub: null,
    });
  });

  test("shows inline errors from backend validation", async () => {
    mockGetProfile.mockResolvedValue({
      contact_email: null,
      telegram: null,
      signal: null,
      nostr_npub: null,
    });
    // Import ApiError from the actual module (not mocked)
    const { ApiError } = await import("../api/client");
    mockUpdateProfile.mockRejectedValue(
      new ApiError("Validation failed", 422, {
        detail: {
          field_errors: {
            telegram: "Backend error: invalid handle",
          },
        },
      })
    );

    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 () => {
    mockGetProfile.mockResolvedValue({
      contact_email: null,
      telegram: null,
      signal: null,
      nostr_npub: null,
    });
    mockUpdateProfile.mockRejectedValue(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: {
      contact_email: string | null;
      telegram: string | null;
      signal: string | null;
      nostr_npub: string | null;
    }) => void;
    const submitPromise = new Promise<{
      contact_email: string | null;
      telegram: string | null;
      signal: string | null;
      nostr_npub: string | null;
    }>((resolve) => {
      resolveSubmit = resolve;
    });

    mockGetProfile.mockResolvedValue({
      contact_email: null,
      telegram: null,
      signal: null,
      nostr_npub: null,
    });
    mockUpdateProfile.mockReturnValue(submitPromise);

    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!({
      contact_email: "new@example.com",
      telegram: null,
      signal: null,
      nostr_npub: null,
    });

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