import { afterEach, describe, expect, test, vi } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/svelte';
import { pageUrlModule } from '$test-utils/mocks/appState';

// vi.hoisted avoids the temporal-dead-zone hazard: mock factories are lazy
// and can run before top-level `let` is initialized, since imports are
// hoisted above variable declarations.
const state = vi.hoisted(() => ({ pageUrl: new URL('http://localhost/login') }));

vi.mock('$app/state', () => pageUrlModule(state));

vi.mock('$app/navigation', () => ({
  goto: vi.fn()
}));

vi.mock('$lib/auth/store.svelte', () => ({
  login: vi.fn(),
  user: { value: null }
}));

import LoginPage from './+page.svelte';
import { login } from '$lib/auth/store.svelte';
import { goto } from '$app/navigation';

afterEach(() => {
  vi.clearAllMocks();
  state.pageUrl = new URL('http://localhost/login');
});

async function submit(username = 'alice', password = 'pw') {
  const u = screen.getByLabelText(/username/i) as HTMLInputElement;
  const p = screen.getByLabelText(/password/i) as HTMLInputElement;
  await fireEvent.input(u, { target: { value: username } });
  await fireEvent.input(p, { target: { value: password } });
  await fireEvent.click(screen.getByRole('button', { name: /sign in/i }));
}

describe('login page', () => {
  test('renders username, password, and sign-in button', () => {
    render(LoginPage);
    expect(screen.getByLabelText(/username/i)).toBeInTheDocument();
    expect(screen.getByLabelText(/password/i)).toBeInTheDocument();
    expect(screen.getByRole('button', { name: /sign in/i })).toBeInTheDocument();
  });

  test('renders a link to the register page', () => {
    render(LoginPage);
    const link = screen.getByRole('link', { name: /register/i });
    expect(link).toHaveAttribute('href', '/register');
  });

  test('invalid credentials show inline error and clear password', async () => {
    (login as ReturnType<typeof vi.fn>).mockRejectedValue({
      code: 'invalid_credentials', message: 'bad creds', status: 401
    });
    render(LoginPage);
    await submit();
    await waitFor(() => {
      expect(screen.getByRole('alert')).toHaveTextContent(/invalid username or password/i);
    });
    expect((screen.getByLabelText(/password/i) as HTMLInputElement).value).toBe('');
    expect((screen.getByLabelText(/username/i) as HTMLInputElement).value).toBe('alice');
  });

  test('server error shows generic message and preserves both fields', async () => {
    (login as ReturnType<typeof vi.fn>).mockRejectedValue({
      code: 'server_error', message: 'boom', status: 500
    });
    render(LoginPage);
    await submit('alice', 'pw');
    await waitFor(() => {
      expect(screen.getByRole('alert')).toHaveTextContent(/try again in a moment/i);
    });
    expect((screen.getByLabelText(/password/i) as HTMLInputElement).value).toBe('pw');
    expect((screen.getByLabelText(/username/i) as HTMLInputElement).value).toBe('alice');
  });

  test('success navigates to returnTo when safe', async () => {
    state.pageUrl = new URL('http://localhost/login?returnTo=/artists/abc');
    (login as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
    render(LoginPage);
    await submit();
    await waitFor(() => {
      expect(goto).toHaveBeenCalledWith('/artists/abc', { replaceState: true });
    });
  });

  test('success falls back to "/" when returnTo is missing', async () => {
    (login as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
    render(LoginPage);
    await submit();
    await waitFor(() => {
      expect(goto).toHaveBeenCalledWith('/', { replaceState: true });
    });
  });

  test.each([
    ['//evil.com',    'protocol-relative'],
    ['http://evil.com', 'absolute URL'],
    ['/login',        'self-redirect'],
    ['../admin',      'parent traversal']
  ])('rejects unsafe returnTo %s (%s) and falls back to "/"', async (value) => {
    state.pageUrl = new URL(`http://localhost/login?returnTo=${encodeURIComponent(value)}`);
    (login as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
    render(LoginPage);
    await submit();
    await waitFor(() => {
      expect(goto).toHaveBeenCalledWith('/', { replaceState: true });
    });
  });

  test('submit button disables and marks aria-busy while pending', async () => {
    let release!: () => void;
    (login as ReturnType<typeof vi.fn>).mockReturnValue(
      new Promise<void>((resolve) => { release = () => resolve(); })
    );
    render(LoginPage);
    await submit();
    const btn = screen.getByRole('button', { name: /sign in/i });
    expect(btn).toBeDisabled();
    expect(btn).toHaveAttribute('aria-busy', 'true');
    release();
    await waitFor(() => expect(btn).not.toBeDisabled());
  });
});
