import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { Membership } from '@alqove/api-client';
import InvitationAcceptPage from '../page';

const mocks = vi.hoisted(() => ({
  replace: vi.fn(), accept: vi.fn(),
  state: {
    user: { id: 'u1', email: 'invited@example.com' } as { id: string; email: string } | null,
    token: 'session', isLoading: false, memberships: [] as Membership[] | null,
    selectStore: vi.fn(), refreshMemberships: vi.fn(), logout: vi.fn(),
  },
}));
vi.mock('next/navigation', () => ({ useRouter: () => ({ replace: mocks.replace }) }));
vi.mock('@/stores/auth', () => ({ captureAuthEpoch: () => 1, useAuthStore: Object.assign(() => mocks.state, { getState: () => mocks.state }) }));
vi.mock('@/lib/api', () => ({ api: { team: { invitations: { accept: (...args: unknown[]) => mocks.accept(...args) } } } }));
const membership: Membership = { id: 'm1', store_id: 's1', store_name: 'North', role: 'sales', capabilities: ['team.view'], is_exempt: false };

beforeEach(() => {
  vi.restoreAllMocks();
  vi.clearAllMocks();
  sessionStorage.clear();
  window.history.replaceState(null, '', '/invitations/accept?token=invite-secret');
  Object.assign(mocks.state, { user: { id: 'u1', email: 'invited@example.com' }, token: 'session', isLoading: false, memberships: [] });
  mocks.accept.mockResolvedValue({ data: membership });
  mocks.state.refreshMemberships.mockImplementation(async () => { mocks.state.memberships = [membership]; });
});

describe('invitation acceptance', () => {
  it('fails closed with recovery instructions when tab storage is unavailable', async () => {
    vi.spyOn(Object.getPrototypeOf(sessionStorage), 'setItem').mockImplementation(() => { throw new Error('Denied'); });
    render(<InvitationAcceptPage />);
    expect(await screen.findByRole('alert')).toHaveTextContent(/enable session storage/i);
    expect(window.location.search).toBe('');
    expect(screen.queryByRole('button', { name: 'Accept invitation' })).not.toBeInTheDocument();
    expect(mocks.accept).not.toHaveBeenCalled();
  });
  it('lets the recipient change accounts without losing the invitation', async () => {
    render(<InvitationAcceptPage />);
    expect(await screen.findByText(/invited@example.com/)).toBeInTheDocument();
    fireEvent.click(screen.getByRole('button', { name: 'Use a different account' }));
    await waitFor(() => expect(mocks.state.logout).toHaveBeenCalledOnce());
    expect(mocks.replace).toHaveBeenCalledWith('/login?next=/invitations/accept');
    expect(sessionStorage.getItem('alqove.pending-invitation')).toBe('invite-secret');
    expect(mocks.accept).not.toHaveBeenCalled();
  });
  it('clears the pending token when the recipient cancels', async () => {
    mocks.state.user = null;
    render(<InvitationAcceptPage />);
    fireEvent.click(await screen.findByRole('button', { name: 'Cancel invitation' }));
    expect(sessionStorage.getItem('alqove.pending-invitation')).toBeNull();
    expect(mocks.replace).toHaveBeenCalledWith('/');
    expect(mocks.accept).not.toHaveBeenCalled();
  });
  it('does not navigate or select a store after the page is unmounted', async () => {
    let resolve!: (value: { data: Membership }) => void;
    mocks.accept.mockReturnValue(new Promise(r => { resolve = r; }));
    const { unmount } = render(<InvitationAcceptPage />);
    fireEvent.click(await screen.findByRole('button', { name: 'Accept invitation' }));
    unmount();
    await act(async () => { resolve({ data: membership }); });
    expect(mocks.state.refreshMemberships).not.toHaveBeenCalled();
    expect(mocks.state.selectStore).not.toHaveBeenCalled();
    expect(mocks.replace).not.toHaveBeenCalled();
  });
  it('locks duplicate acceptance and ignores the result after logout', async () => {
    let resolve!: (value: { data: Membership }) => void;
    mocks.accept.mockReturnValue(new Promise(r => { resolve = r; }));
    render(<InvitationAcceptPage />);
    const button = await screen.findByRole('button', { name: 'Accept invitation' });
    fireEvent.click(button);
    fireEvent.click(button);
    expect(mocks.accept).toHaveBeenCalledOnce();
    expect(button).toBeDisabled();
    mocks.state.user = null;
    await act(async () => { resolve({ data: membership }); });
    expect(mocks.state.refreshMemberships).not.toHaveBeenCalled();
    expect(mocks.state.selectStore).not.toHaveBeenCalled();
    expect(mocks.replace).not.toHaveBeenCalled();
  });
  it('does not switch stores if the account changes during refresh', async () => {
    mocks.state.refreshMemberships.mockImplementationOnce(async () => {
      mocks.state.user = { id: 'u2', email: 'other@example.com' };
      mocks.state.memberships = [membership];
    });
    render(<InvitationAcceptPage />);
    fireEvent.click(await screen.findByRole('button', { name: 'Accept invitation' }));
    await waitFor(() => expect(mocks.state.refreshMemberships).toHaveBeenCalled());
    expect(mocks.state.selectStore).not.toHaveBeenCalled();
    expect(mocks.replace).not.toHaveBeenCalled();
  });
  it.each([[null], [[]]])('does not enter staff until a refresh confirms membership: %j', async (memberships) => {
    mocks.state.refreshMemberships.mockImplementationOnce(async () => { mocks.state.memberships = memberships; });
    render(<InvitationAcceptPage />);
    fireEvent.click(await screen.findByRole('button', { name: 'Accept invitation' }));
    expect(await screen.findByRole('alert')).toHaveTextContent(/could not verify store access/i);
    expect(mocks.replace).not.toHaveBeenCalled();
    expect(mocks.state.selectStore).not.toHaveBeenCalled();
    expect(sessionStorage.getItem('alqove.pending-invitation')).toBeNull();
    fireEvent.click(screen.getByRole('button', { name: 'Retry store access' }));
    await waitFor(() => expect(mocks.replace).toHaveBeenCalledWith('/staff'));
    expect(mocks.accept).toHaveBeenCalledOnce();
    expect(mocks.state.selectStore).toHaveBeenCalledWith('s1');
  });
  it.each([403, 404, 409, 410, 422, 429, 500])('does not report success or expose a token on API failure %s', async (status) => {
    mocks.accept.mockRejectedValue({ status, message: 'Rejected token invite-secret' });
    render(<InvitationAcceptPage />);
    fireEvent.click(await screen.findByRole('button', { name: 'Accept invitation' }));
    expect(await screen.findByRole('alert')).toHaveTextContent(/could not accept/i);
    expect(screen.queryByText(/invite-secret/)).not.toBeInTheDocument();
    expect(mocks.state.refreshMemberships).not.toHaveBeenCalled();
    expect(mocks.state.selectStore).not.toHaveBeenCalled();
    expect(mocks.replace).not.toHaveBeenCalled();
    expect(sessionStorage.getItem('alqove.pending-invitation')).toBe('invite-secret');
  });
  it('offers token-free sign-in and registration links before accepting', async () => {
    mocks.state.user = null;
    render(<InvitationAcceptPage />);
    expect(await screen.findByRole('link', { name: 'Sign in' })).toHaveAttribute('href', '/login?next=/invitations/accept');
    expect(screen.getByRole('link', { name: 'Create an account' })).toHaveAttribute('href', '/register?next=/invitations/accept');
    expect(screen.queryByRole('button', { name: 'Accept invitation' })).not.toBeInTheDocument();
    expect(mocks.accept).not.toHaveBeenCalled();
    expect(sessionStorage.getItem('alqove.pending-invitation')).toBe('invite-secret');
  });
  it('does not offer acceptance when the invitation is missing', async () => {
    window.history.replaceState(null, '', '/invitations/accept');
    render(<InvitationAcceptPage />);
    expect(await screen.findByRole('alert')).toHaveTextContent(/invitation link is missing/i);
    expect(screen.queryByRole('button', { name: 'Accept invitation' })).not.toBeInTheDocument();
  });
  it('requires explicit acceptance, refreshes authoritative memberships and selects the invited store', async () => {
    render(<InvitationAcceptPage />);
    const accept = await screen.findByRole('button', { name: 'Accept invitation' });
    expect(mocks.accept).not.toHaveBeenCalled();
    expect(window.location.search).toBe('');
    fireEvent.click(accept);
    await waitFor(() => expect(mocks.replace).toHaveBeenCalledWith('/staff'));
    expect(mocks.accept).toHaveBeenCalledExactlyOnceWith('invite-secret');
    expect(mocks.state.refreshMemberships).toHaveBeenCalledOnce();
    expect(mocks.state.selectStore).toHaveBeenCalledWith('s1');
    expect(sessionStorage.getItem('alqove.pending-invitation')).toBeNull();
  });
});
