import { act, renderHook, waitFor } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

const mocks = vi.hoisted(() => ({
  create: vi.fn(),
  getFullList: vi.fn(),
  authStore: {
    isValid: true,
    record: { id: 'user-1' } as { id: string } | null,
    onChange: () => () => {},
  },
}));

vi.mock('../lib/pocketbase', () => ({
  default: {
    authStore: mocks.authStore,
    collection: () => ({ getFullList: mocks.getFullList, create: mocks.create }),
  },
}));

import { useClickedListings } from './useClickedListings';

describe('useClickedListings', () => {
  beforeEach(() => {
    mocks.create.mockReset().mockResolvedValue({});
    mocks.getFullList.mockReset().mockResolvedValue([]);
    mocks.authStore.isValid = true;
    mocks.authStore.record = { id: 'user-1' };
  });

  afterEach(() => {
    vi.clearAllMocks();
  });

  it('hydrates the visited set from PocketBase for the signed-in user', async () => {
    mocks.getFullList.mockResolvedValue([{ url: 'https://example.com/a' }]);
    const { result } = renderHook(() => useClickedListings());

    await waitFor(() => expect(result.current.clickedUrls.has('https://example.com/a')).toBe(true));
  });

  it('records a click optimistically and persists it', async () => {
    const { result } = renderHook(() => useClickedListings());
    await waitFor(() => expect(mocks.getFullList).toHaveBeenCalled());

    act(() => result.current.markClicked('https://example.com/b'));

    // Visible immediately — no need to await the network round-trip.
    expect(result.current.clickedUrls.has('https://example.com/b')).toBe(true);
    expect(mocks.create).toHaveBeenCalledWith({
      user: 'user-1',
      url: 'https://example.com/b',
    });
  });

  it('produces a new Set instance for a new url so deck.gl re-colours', async () => {
    const { result } = renderHook(() => useClickedListings());
    await waitFor(() => expect(mocks.getFullList).toHaveBeenCalled());

    const before = result.current.clickedUrls;
    act(() => result.current.markClicked('https://example.com/c'));
    expect(result.current.clickedUrls).not.toBe(before);
  });

  it('does not re-persist an already visited url', async () => {
    mocks.getFullList.mockResolvedValue([{ url: 'https://example.com/a' }]);
    const { result } = renderHook(() => useClickedListings());
    await waitFor(() => expect(result.current.clickedUrls.has('https://example.com/a')).toBe(true));

    const before = result.current.clickedUrls;
    act(() => result.current.markClicked('https://example.com/a'));

    expect(mocks.create).not.toHaveBeenCalled();
    expect(result.current.clickedUrls).toBe(before); // identity stable → no needless recompute
  });

  it('ignores empty urls', async () => {
    const { result } = renderHook(() => useClickedListings());
    await waitFor(() => expect(mocks.getFullList).toHaveBeenCalled());

    act(() => result.current.markClicked(''));
    act(() => result.current.markClicked(undefined));

    expect(mocks.create).not.toHaveBeenCalled();
    expect(result.current.clickedUrls.size).toBe(0);
  });

  it('recolours for anonymous users in-memory but does not persist', async () => {
    mocks.authStore.isValid = false;
    mocks.authStore.record = null;
    const { result } = renderHook(() => useClickedListings());

    act(() => result.current.markClicked('https://example.com/d'));

    expect(result.current.clickedUrls.has('https://example.com/d')).toBe(true);
    expect(mocks.create).not.toHaveBeenCalled();
  });
});
