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

import { useHexagonSelection } from './useHexagonSelection';
import type { FeatureMeta, HexagonStatsResponse, PostcodeGeometry } from '../types';
import type { TravelTimeEntry } from './useTravelTime';

vi.mock('../lib/pocketbase', () => ({
  default: { authStore: { isValid: false, token: '' } },
}));

vi.mock('../lib/analytics', () => ({
  trackEvent: vi.fn(),
}));

const postcodeGeometry: PostcodeGeometry = {
  type: 'Polygon',
  coordinates: [
    [
      [-0.12, 51.5],
      [-0.11, 51.5],
      [-0.11, 51.51],
      [-0.12, 51.51],
      [-0.12, 51.5],
    ],
  ],
};

function stats(count: number): HexagonStatsResponse {
  return {
    count,
    numeric_features: [],
    enum_features: [],
    central_postcode: 'SW1A 1AA',
  };
}

function jsonResponse(body: unknown): Response {
  return new Response(JSON.stringify(body), {
    status: 200,
    headers: { 'Content-Type': 'application/json' },
  });
}

async function flushPromises() {
  await Promise.resolve();
  await Promise.resolve();
}

describe('useHexagonSelection', () => {
  const requests: string[] = [];
  const features: FeatureMeta[] = [
    { name: 'Price', type: 'numeric', min: 0, max: 100 },
    { name: 'Last known price', type: 'numeric', min: 0, max: 1_000_000 },
    { name: 'Estimated current price', type: 'numeric', min: 0, max: 1_000_000 },
    { name: 'Price per sqm', type: 'numeric', min: 0, max: 20_000 },
    { name: 'Est. price per sqm', type: 'numeric', min: 0, max: 20_000 },
    { name: 'Total floor area (sqm)', type: 'numeric', min: 0, max: 500 },
    { name: 'Number of bedrooms & living rooms', type: 'numeric', min: 0, max: 12 },
    { name: 'Construction year', type: 'numeric', min: 0, max: 2026 },
    { name: 'Date of last transaction', type: 'numeric', min: 0, max: 2026 },
  ];

  beforeEach(() => {
    requests.length = 0;
    vi.stubGlobal(
      'fetch',
      vi.fn((input: string | URL | Request) => {
        const url = new URL(String(input), 'http://localhost');
        requests.push(`${url.pathname}${url.search}`);

        if (url.pathname === '/api/postcode-stats') {
          const emptyPostcode = url.searchParams.get('postcode') === 'EMPTY 1AA';
          return Promise.resolve(
            jsonResponse(stats(url.searchParams.has('filters') || emptyPostcode ? 0 : 4))
          );
        }

        if (url.pathname === '/api/hexagon-stats') {
          return Promise.resolve(jsonResponse(stats(12)));
        }

        if (url.pathname.startsWith('/api/postcode/')) {
          const postcode = decodeURIComponent(url.pathname.slice('/api/postcode/'.length));
          return Promise.resolve(
            jsonResponse({
              postcode,
              latitude: 51.5,
              longitude: -0.115,
              geometry: postcodeGeometry,
            })
          );
        }

        if (url.pathname === '/api/postcode-properties') {
          return Promise.resolve(
            jsonResponse({ properties: [], total: 0, offset: 0, truncated: false })
          );
        }

        if (url.pathname === '/api/hexagon-properties') {
          return Promise.resolve(
            jsonResponse({ properties: [], total: 0, offset: 0, truncated: false })
          );
        }

        return Promise.resolve(new Response(null, { status: 404 }));
      })
    );
  });

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

  it('keeps a postcode search selected when filters exclude its properties', async () => {
    const { result } = renderHook(() =>
      useHexagonSelection({
        filters: { Price: [0, 50] },
        features,
        hexagonData: [],
        resolution: 9,
        usePostcodeView: true,
        travelTimeEntries: [],
      })
    );

    act(() => {
      result.current.handleLocationSearch('SW1A 1AA', postcodeGeometry, 51.505, -0.115);
    });

    await waitFor(() => {
      expect(result.current.selectedHexagon).toEqual({
        id: 'SW1A 1AA',
        type: 'postcode',
        resolution: 9,
        lockedResolution: true,
      });
    });

    expect(result.current.selectedPostcodeGeometry).toBe(postcodeGeometry);
    await waitFor(() => {
      expect(result.current.areaStats?.count).toBe(0);
      expect(result.current.unfilteredAreaCount).toBe(4);
    });
    expect(requests.some((url) => url.startsWith('/api/hexagon-stats'))).toBe(false);
  });

  it('keeps a postcode search selected when stats are based on all properties', async () => {
    const { result } = renderHook(() =>
      useHexagonSelection({
        filters: { Price: [0, 50] },
        features,
        hexagonData: [],
        resolution: 9,
        usePostcodeView: true,
        travelTimeEntries: [],
      })
    );

    act(() => {
      result.current.setAreaStatsUseFilters(false);
    });
    act(() => {
      result.current.handleLocationSearch('SW1A 1AA', postcodeGeometry, 51.505, -0.115);
    });

    await waitFor(() => {
      expect(result.current.selectedHexagon).toEqual({
        id: 'SW1A 1AA',
        type: 'postcode',
        resolution: 9,
        lockedResolution: true,
      });
    });

    await waitFor(() => {
      expect(result.current.areaStats?.count).toBe(4);
      expect(result.current.unfilteredAreaCount).toBeNull();
    });
    expect(requests.some((url) => url.startsWith('/api/hexagon-stats'))).toBe(false);
  });

  it('keeps an empty postcode search selected instead of widening to hexagons', async () => {
    const { result } = renderHook(() =>
      useHexagonSelection({
        filters: {},
        features,
        hexagonData: [],
        resolution: 9,
        usePostcodeView: true,
        travelTimeEntries: [],
      })
    );

    act(() => {
      result.current.handleLocationSearch('EMPTY 1AA', postcodeGeometry, 51.505, -0.115);
    });

    await waitFor(() => {
      expect(result.current.areaStats?.count).toBe(0);
    });

    expect(result.current.selectedHexagon).toEqual({
      id: 'EMPTY 1AA',
      type: 'postcode',
      resolution: 9,
      lockedResolution: true,
    });
    expect(result.current.selectedPostcodeGeometry).toBe(postcodeGeometry);
    expect(requests.some((url) => url.startsWith('/api/hexagon-stats'))).toBe(false);
  });

  it('does not convert a searched postcode back to a hexagon while the map reaches postcode zoom', async () => {
    const { result } = renderHook(() =>
      useHexagonSelection({
        filters: {},
        features,
        hexagonData: [],
        resolution: 9,
        usePostcodeView: false,
        travelTimeEntries: [],
      })
    );

    act(() => {
      result.current.handleLocationSearch('SW1A 1AA', postcodeGeometry, 51.505, -0.115);
    });

    await waitFor(() => {
      expect(result.current.areaStats?.count).toBe(4);
    });

    expect(result.current.selectedHexagon).toEqual({
      id: 'SW1A 1AA',
      type: 'postcode',
      resolution: 9,
      lockedResolution: true,
    });
    expect(result.current.selectedPostcodeGeometry).toBe(postcodeGeometry);
    expect(requests.some((url) => url.startsWith('/api/postcode/'))).toBe(false);
    expect(requests.some((url) => url.startsWith('/api/hexagon-stats'))).toBe(false);
  });

  it('passes area stat field projections to stats requests', async () => {
    const { result } = renderHook(() =>
      useHexagonSelection({
        filters: {},
        features,
        hexagonData: [],
        resolution: 9,
        usePostcodeView: false,
        travelTimeEntries: [],
        areaStatsFields: ['Price'],
      })
    );

    act(() => {
      result.current.handleHexagonClick('89195da49abffff');
    });

    await waitFor(() => {
      expect(result.current.areaStats?.count).toBe(12);
    });

    const statsRequest = requests.find((url) => url.startsWith('/api/hexagon-stats'));
    expect(statsRequest).toBeDefined();
    expect(new URL(statsRequest!, 'http://localhost').searchParams.get('fields')).toBe('Price');
  });

  it('keeps existing area stats visible while area field projections refetch', async () => {
    const pendingStatsRequests: Array<{ resolve: (response: Response) => void }> = [];
    vi.stubGlobal(
      'fetch',
      vi.fn((input: string | URL | Request) => {
        const url = new URL(String(input), 'http://localhost');
        requests.push(`${url.pathname}${url.search}`);

        if (url.pathname === '/api/hexagon-stats') {
          return new Promise<Response>((resolve) => {
            pendingStatsRequests.push({ resolve });
          });
        }

        return Promise.resolve(new Response(null, { status: 404 }));
      })
    );

    const { result, rerender } = renderHook(
      ({ areaStatsFields }: { areaStatsFields: string[] }) =>
        useHexagonSelection({
          filters: {},
          features,
          hexagonData: [],
          resolution: 9,
          usePostcodeView: false,
          travelTimeEntries: [],
          areaStatsFields,
        }),
      { initialProps: { areaStatsFields: [] as string[] } }
    );

    act(() => {
      result.current.handleHexagonClick('89195da49abffff');
    });

    await waitFor(() => {
      expect(pendingStatsRequests).toHaveLength(1);
    });

    await act(async () => {
      pendingStatsRequests[0].resolve(jsonResponse(stats(12)));
      await flushPromises();
    });

    await waitFor(() => {
      expect(result.current.areaStats?.count).toBe(12);
      expect(result.current.loadingAreaStats).toBe(false);
    });

    act(() => {
      rerender({ areaStatsFields: ['Price'] });
    });

    await waitFor(() => {
      expect(pendingStatsRequests).toHaveLength(2);
    });

    expect(result.current.loadingAreaStats).toBe(true);
    expect(result.current.areaStats?.count).toBe(12);

    const refetchRequest = requests.filter((url) => url.startsWith('/api/hexagon-stats'))[1];
    expect(new URL(refetchRequest, 'http://localhost').searchParams.get('fields')).toBe('Price');

    await act(async () => {
      pendingStatsRequests[1].resolve(jsonResponse(stats(12)));
      await flushPromises();
    });

    await waitFor(() => {
      expect(result.current.loadingAreaStats).toBe(false);
      expect(result.current.areaStats?.count).toBe(12);
    });
  });

  it('passes property card field projections to property requests', async () => {
    const { result } = renderHook(() =>
      useHexagonSelection({
        filters: {},
        features,
        hexagonData: [],
        resolution: 9,
        usePostcodeView: true,
        travelTimeEntries: [],
      })
    );

    act(() => {
      result.current.handleLocationSearch('SW1A 1AA', postcodeGeometry, 51.505, -0.115);
    });

    await waitFor(() => {
      expect(result.current.areaStats?.count).toBe(4);
    });

    act(() => {
      result.current.handlePropertiesTabClick();
    });

    await waitFor(() => {
      expect(requests.some((url) => url.startsWith('/api/postcode-properties'))).toBe(true);
    });

    const propertiesRequest = requests.find((url) => url.startsWith('/api/postcode-properties'));
    const fieldsParam = new URL(propertiesRequest!, 'http://localhost').searchParams.get('fields');
    expect(fieldsParam).toContain('Last known price');
    expect(fieldsParam).toContain('Date of last transaction');
    expect(fieldsParam).not.toContain('Distance to nearest amenity');
  });

  it('refetches property requests when stats basis switches to all properties', async () => {
    const propertyFilters = { Price: [0, 50] as [number, number] };
    const travelTimeEntries: TravelTimeEntry[] = [
      {
        mode: 'transit',
        slug: 'kings-cross',
        label: 'Kings Cross',
        timeRange: [0, 30],
        useBest: false,
      },
    ];
    const { result } = renderHook(() =>
      useHexagonSelection({
        filters: propertyFilters,
        features,
        hexagonData: [],
        resolution: 9,
        usePostcodeView: true,
        travelTimeEntries,
      })
    );

    act(() => {
      result.current.handleLocationSearch('SW1A 1AA', postcodeGeometry, 51.505, -0.115);
    });

    await waitFor(() => {
      expect(result.current.areaStats?.count).toBe(0);
    });

    act(() => {
      result.current.handlePropertiesTabClick();
    });

    await waitFor(() => {
      expect(requests.filter((url) => url.startsWith('/api/postcode-properties')).length).toBe(1);
    });

    const filteredPropertiesRequest = requests.find((url) =>
      url.startsWith('/api/postcode-properties')
    );
    const filteredParams = new URL(filteredPropertiesRequest!, 'http://localhost').searchParams;
    expect(filteredParams.has('filters')).toBe(true);
    expect(filteredParams.has('travel')).toBe(true);

    act(() => {
      result.current.setAreaStatsUseFilters(false);
    });

    await waitFor(() => {
      expect(result.current.areaStats?.count).toBe(4);
    });
    await waitFor(() => {
      expect(requests.filter((url) => url.startsWith('/api/postcode-properties')).length).toBe(2);
    });

    const propertyRequests = requests.filter((url) => url.startsWith('/api/postcode-properties'));
    const allPropertiesRequest = propertyRequests[propertyRequests.length - 1];
    const allPropertiesParams = new URL(allPropertiesRequest, 'http://localhost').searchParams;
    expect(allPropertiesParams.has('filters')).toBe(false);
    expect(allPropertiesParams.has('travel')).toBe(false);
  });

  it('restores the original clicked postcode after zooming out to a hexagon and back', async () => {
    // Stable references: the sync effect synchronously resets properties when it
    // runs, so unstable filters/hexagonData/travel props (new identity each
    // render) would re-trigger it forever. In the app these come from memoised
    // hooks; mirror that here.
    const stableFilters = {};
    const stableHexagonData: never[] = [];
    const stableTravel: TravelTimeEntry[] = [];
    const { result, rerender } = renderHook(
      ({ usePostcodeView }: { usePostcodeView: boolean }) =>
        useHexagonSelection({
          filters: stableFilters,
          features,
          hexagonData: stableHexagonData,
          resolution: 9,
          usePostcodeView,
          travelTimeEntries: stableTravel,
        }),
      { initialProps: { usePostcodeView: true } }
    );

    // Click a postcode whose neighbouring hexagon reports a *different*
    // central_postcode ('SW1A 1AA', see stats()).
    act(() => {
      result.current.handleHexagonClick('AB1 2CD', true, postcodeGeometry);
    });

    await waitFor(() => {
      expect(result.current.selectedHexagon?.id).toBe('AB1 2CD');
      expect(result.current.selectedHexagon?.type).toBe('postcode');
    });
    expect(result.current.selectedHexagon?.anchorPostcode?.id).toBe('AB1 2CD');

    // Zoom out past the postcode threshold: the selection follows down to a hexagon.
    act(() => {
      rerender({ usePostcodeView: false });
    });

    await waitFor(() => {
      expect(result.current.selectedHexagon?.type).toBe('hexagon');
    });
    expect(result.current.selectedHexagon?.anchorPostcode?.id).toBe('AB1 2CD');

    // Zoom back in: the original postcode must be restored, not the hexagon's
    // central_postcode ('SW1A 1AA').
    act(() => {
      rerender({ usePostcodeView: true });
    });

    await waitFor(() => {
      expect(result.current.selectedHexagon?.type).toBe('postcode');
    });
    expect(result.current.selectedHexagon?.id).toBe('AB1 2CD');
    expect(result.current.selectedPostcodeGeometry).toBe(postcodeGeometry);
  });
});
