import { describe, expect, it } from 'vitest';
import { existsSync, readFileSync } from 'fs';
import { join } from 'path';

import {
  DENSITY_GRADIENT,
  ENUM_PALETTE,
  FEATURE_GRADIENT,
  BUFFER_MULTIPLIER,
  MAP_BOUNDS,
  POI_CATEGORY_LOGOS,
  SMALLEST_VISIBLE_HEXAGON_RESOLUTION,
} from './consts';
import {
  emojiToTwemojiUrl,
  enumIndexToColor,
  getBoundsFromViewState,
  getBoundsWithBottomScreenInset,
  getVisibleBoundsFromViewState,
  getLatitudeAtVerticalPixelOffset,
  getFeatureFillColor,
  getMapCenterForTargetScreenPoint,
  getPoiIconUrl,
  zoomToResolution,
} from './map-utils';

describe('map utilities', () => {
  it('maps zoom levels to H3 resolutions at configured thresholds', () => {
    expect(zoomToResolution(6.9)).toBe(5);
    expect(zoomToResolution(7)).toBe(6);
    expect(zoomToResolution(10.6)).toBe(8);
    expect(zoomToResolution(12)).toBe(9);
    expect(zoomToResolution(14)).toBe(9);
    expect(SMALLEST_VISIBLE_HEXAGON_RESOLUTION).toBe(9);
  });

  it('computes exact viewport bounds by default', () => {
    const viewState = { latitude: 51.5, longitude: -0.1, zoom: 12, pitch: 0 };
    const bounds = getBoundsFromViewState(viewState, 1200, 800);
    const exactBounds = getBoundsFromViewState(viewState, 1200, 800, 1);
    const bufferedBounds = getBoundsFromViewState(viewState, 1200, 800, 1.5);

    expect(BUFFER_MULTIPLIER).toBe(1);
    expect(bounds).toEqual(exactBounds);
    expect(bounds.south).toBeLessThan(51.5);
    expect(bounds.north).toBeGreaterThan(51.5);
    expect(bounds.west).toBeLessThan(-0.1);
    expect(bounds.east).toBeGreaterThan(-0.1);
    expect(bufferedBounds.south).toBeLessThan(bounds.south);
    expect(bufferedBounds.north).toBeGreaterThan(bounds.north);
    expect(bufferedBounds.west).toBeLessThan(bounds.west);
    expect(bufferedBounds.east).toBeGreaterThan(bounds.east);
  });

  it('excludes mobile bottom-sheet covered map area from visible bounds', () => {
    const viewState = { latitude: 51.5, longitude: -0.1, zoom: 12, pitch: 0 };
    const fullBounds = getVisibleBoundsFromViewState(viewState, 390, 800, 0);
    const visibleBounds = getVisibleBoundsFromViewState(viewState, 390, 800, 352);

    expect(visibleBounds.west).toBeCloseTo(fullBounds.west, 6);
    expect(visibleBounds.east).toBeCloseTo(fullBounds.east, 6);
    expect(visibleBounds.north).toBeCloseTo(fullBounds.north, 6);
    expect(visibleBounds.south).toBeGreaterThan(fullBounds.south);
  });

  it('moves the map center so a target lands in the requested screen position', () => {
    const centered = getMapCenterForTargetScreenPoint(51.5, -0.1, 17, 390, 844, 195, 42.2);

    expect(centered.longitude).toBeCloseTo(-0.1, 6);
    expect(centered.latitude).toBeLessThan(51.5);
  });

  it('expands the southern map bound by a covered screen area', () => {
    const shiftedSouth = getLatitudeAtVerticalPixelOffset(MAP_BOUNDS[1], 5.5, 320);
    const shiftedNorth = getLatitudeAtVerticalPixelOffset(MAP_BOUNDS[1], 5.5, -320);
    const expandedBounds = getBoundsWithBottomScreenInset(MAP_BOUNDS, 5.5, 320);

    expect(shiftedSouth).toBeLessThan(MAP_BOUNDS[1]);
    expect(shiftedNorth).toBeGreaterThan(MAP_BOUNDS[1]);
    expect(expandedBounds[0]).toBe(MAP_BOUNDS[0]);
    expect(expandedBounds[1]).toBeCloseTo(shiftedSouth, 6);
    expect(expandedBounds[2]).toBe(MAP_BOUNDS[2]);
    expect(expandedBounds[3]).toBe(MAP_BOUNDS[3]);
    expect(getBoundsWithBottomScreenInset(MAP_BOUNDS, 5.5, 0)).toEqual(MAP_BOUNDS);
  });

  it('builds twemoji URLs and wraps enum colors', () => {
    expect(emojiToTwemojiUrl('🛒')).toBe('/assets/twemoji/1f6d2.png');
    expect(() => emojiToTwemojiUrl('')).toThrow('Cannot build a Twemoji URL without an emoji');
    expect(enumIndexToColor(ENUM_PALETTE.length, ENUM_PALETTE)).toEqual(ENUM_PALETTE[0]);
  });

  it('resolves POI category logos and generates a fallback for unknown chains', () => {
    expect(getPoiIconUrl('Waitrose', '🛒')).toBe('/assets/poi-icons/logos/waitrose.svg');
    expect(getPoiIconUrl('Iceland', '🛒', 'The Food Warehouse')).toBe(
      '/assets/poi-icons/logos/the_food_warehouse.png'
    );
    expect(getPoiIconUrl("Sainsbury's", '🛒', undefined, 'Sainsburys Earlsfield Local')).toBe(
      '/assets/poi-icons/brands_2024/sainsburys_local.svg'
    );
    expect(getPoiIconUrl('Costco', '🛒')).toBe('/assets/poi-icons/logos/costco.svg');
    expect(getPoiIconUrl('M&S', '🛒', undefined, 'M&S Simply Food')).toBe(
      '/assets/poi-icons/visuals/mns.svg'
    );
    expect(getPoiIconUrl('Tian Tian', '🛒')).toMatch(/^data:image\/svg\+xml;charset=utf-8,/);
  });

  it('keeps POI icon URLs bundled locally', () => {
    expect(Object.values(POI_CATEGORY_LOGOS).filter((url) => /^https?:\/\//.test(url))).toEqual([]);
    expect(
      Object.values(POI_CATEGORY_LOGOS)
        .filter((url) => url.startsWith('/assets/poi-icons/'))
        .filter((url) => !existsSync(join(process.cwd(), 'public', url.slice(1))))
    ).toEqual([]);
  });

  it('does not use pin-shaped SVGs for branded POI logos', () => {
    const pinSignatures = ['viewBox="0 0 400 520"', 'C 18.914 185.931'];
    const svgUrls = [
      ...new Set(
        Object.values(POI_CATEGORY_LOGOS)
          .filter((url) => url.startsWith('/assets/poi-icons/'))
          .filter((url) => url.endsWith('.svg'))
      ),
    ];

    expect(
      svgUrls.filter((url) => {
        const content = readFileSync(join(process.cwd(), 'public', url.slice(1)), 'utf8');
        return pinSignatures.some((signature) => content.includes(signature));
      })
    ).toEqual([]);
  });

  it('keeps bundled SVG logos large enough for the map icon atlas', () => {
    const svgUrls = [
      ...new Set(
        Object.values(POI_CATEGORY_LOGOS)
          .filter((url) => url.startsWith('/assets/poi-icons/'))
          .filter((url) => url.endsWith('.svg'))
      ),
    ];

    expect(
      svgUrls.filter((url) => {
        const content = readFileSync(join(process.cwd(), 'public', url.slice(1)), 'utf8');
        const width = Number(content.match(/width="([0-9.]+)/)?.[1]);
        const height = Number(content.match(/height="([0-9.]+)/)?.[1]);
        return Math.max(width, height) < 256;
      })
    ).toEqual([]);
  });

  it('returns fallback, filtered, enum, feature, and density colors', () => {
    expect(
      getFeatureFillColor(
        null,
        undefined,
        undefined,
        [0, 100],
        null,
        0,
        DENSITY_GRADIENT,
        false,
        180
      )
    ).toEqual([128, 128, 128, 80]);

    expect(
      getFeatureFillColor(50, 50, 60, [0, 100], [70, 90], 0, DENSITY_GRADIENT, true, 180)
    ).toEqual([60, 55, 50, 60]);

    expect(
      getFeatureFillColor(1, 1, 1, [0, 2], null, 0, DENSITY_GRADIENT, false, 180, 3, ENUM_PALETTE)
    ).toEqual([...ENUM_PALETTE[1], 180]);

    expect(
      getFeatureFillColor(
        0,
        0,
        100,
        [0, 100],
        null,
        0,
        DENSITY_GRADIENT,
        false,
        200,
        0,
        undefined,
        FEATURE_GRADIENT
      )
    ).toEqual([...FEATURE_GRADIENT[0].color, 200]);

    expect(
      getFeatureFillColor(
        undefined,
        undefined,
        undefined,
        null,
        null,
        0,
        DENSITY_GRADIENT,
        false,
        150
      )
    ).toEqual([...DENSITY_GRADIENT[0].color, 150]);
  });
});
