import {describe, expect, test} from 'bun:test'

import {isPrivateAddress} from '../../lib/fetch-guard'
import {normalizeShareUrl, parseLinkEntities} from './link'

describe('normalizeShareUrl', () => {
  test('strips hash and tracking params', () => {
    expect(
      normalizeShareUrl(
        'https://Example.com/place?utm_source=ig&fbclid=x&id=2#section',
      ),
    ).toBe('https://example.com/place?id=2')
  })
})

describe('isPrivateAddress', () => {
  test('blocks private and loopback ranges', () => {
    for (const ip of [
      '127.0.0.1',
      '10.0.0.8',
      '172.16.4.1',
      '192.168.1.1',
      '169.254.169.254',
      '0.0.0.0',
      '100.64.0.1',
      '::1',
      '::ffff:10.0.0.1',
      'fd12::1',
      'fe80::1',
    ]) {
      expect(isPrivateAddress(ip)).toBe(true)
    }
  })

  test('allows public addresses', () => {
    for (const ip of [
      '1.1.1.1',
      '142.250.72.78',
      '2606:4700::1111',
    ]) {
      expect(isPrivateAddress(ip)).toBe(false)
    }
  })
})

describe('parseLinkEntities', () => {
  test('parses a json-ld place', () => {
    const html = `<html><head><script type="application/ld+json">
      {"@context":"https://schema.org","@type":"Restaurant",
       "name":"Luigi&#39;s","address":{"streetAddress":"1 Main St",
       "addressLocality":"Brooklyn"},"geo":{"latitude":"40.7","longitude":"-73.9"}}
    </script></head></html>`
    const [entity] = parseLinkEntities(html)
    expect(entity).toMatchObject({
      kind: 'place',
      name: "Luigi's",
      addressHint: '1 Main St, Brooklyn',
      category: 'restaurant',
      latitude: 40.7,
      longitude: -73.9,
    })
  })

  test('parses json-ld events with venues', () => {
    const html = `<script type="application/ld+json">
      [{"@type":"MusicEvent","name":"Night show",
        "startDate":"2026-07-01T20:00:00Z",
        "location":{"@type":"Place","name":"The Hall",
          "address":"12 Stage Rd, Lisbon"}},
       {"@type":"MusicEvent","name":"Day show",
        "startDate":"2026-07-02T14:00:00Z",
        "location":{"@type":"Place","name":"The Hall"}}]
    </script>`
    const entities = parseLinkEntities(html)
    expect(entities).toHaveLength(2)
    expect(entities[0]).toMatchObject({
      kind: 'event',
      name: 'Night show',
      venueName: 'The Hall',
      addressHint: '12 Stage Rd, Lisbon',
      startsAt: '2026-07-01T20:00:00.000Z',
    })
  })

  test('treats offsetless event datetimes as utc', () => {
    const html = `<script type="application/ld+json">
      {"@type":"Event","name":"Local show",
       "startDate":"2026-07-01T20:00:00",
       "endDate":"2026-07-01T22:30:00",
       "location":{"@type":"Place","name":"The Hall"}}
    </script>`
    expect(parseLinkEntities(html)[0]).toMatchObject({
      startsAt: '2026-07-01T20:00:00.000Z',
      endsAt: '2026-07-01T22:30:00.000Z',
    })
  })

  test('keeps explicit offsets on event datetimes', () => {
    const html = `<script type="application/ld+json">
      {"@type":"Event","name":"Offset show",
       "startDate":"2026-07-01T20:00:00+02:00",
       "location":{"@type":"Place","name":"The Hall"}}
    </script>`
    const [entity] = parseLinkEntities(html)
    expect(entity?.kind).toBe('event')
    expect(entity?.kind === 'event' && entity.startsAt).toBe(
      '2026-07-01T18:00:00.000Z',
    )
  })

  test('walks @graph wrappers', () => {
    const html = `<script type="application/ld+json">
      {"@context":"https://schema.org","@graph":[
        {"@type":"Cafe","name":"Slow Bloom","address":"Oakland"}]}
    </script>`
    expect(parseLinkEntities(html)[0]).toMatchObject({
      kind: 'place',
      name: 'Slow Bloom',
      addressHint: 'Oakland',
    })
  })

  test('falls back to opengraph', () => {
    const html = `<head>
      <meta property="og:title" content="Hidden beach &amp; cove" />
      <meta property="og:description" content="A guide" />
      <meta property="og:image" content="https://cdn.example.com/cove.jpg" />
      <title>ignored</title></head>`
    expect(parseLinkEntities(html)).toEqual([
      {
        kind: 'page',
        name: 'Hidden beach & cove',
        description: 'A guide',
        imageUrl: 'https://cdn.example.com/cove.jpg',
      },
    ])
  })

  test('reads twitter:image when og:image is absent', () => {
    const html = `<head>
      <meta name="twitter:image" content="https://cdn.example.com/t.jpg" />
      <title>A place</title></head>`
    expect(parseLinkEntities(html)[0]?.imageUrl).toBe(
      'https://cdn.example.com/t.jpg',
    )
  })

  test('reads json-ld image (string or ImageObject)', () => {
    const html = `<script type="application/ld+json">
      {"@type":"Restaurant","name":"Luigi's","address":"Brooklyn",
       "image":{"@type":"ImageObject","url":"https://cdn.example.com/l.jpg"}}
    </script>`
    expect(parseLinkEntities(html)[0]?.imageUrl).toBe(
      'https://cdn.example.com/l.jpg',
    )
  })

  test('falls back to the document title', () => {
    expect(
      parseLinkEntities('<title> Some page </title>')[0],
    ).toMatchObject({kind: 'page', name: 'Some page'})
  })

  test('skips malformed json-ld', () => {
    const html = `<script type="application/ld+json">{nope</script>
      <meta property="og:title" content="Still works" />`
    expect(parseLinkEntities(html)[0]?.name).toBe('Still works')
  })

  test('returns nothing for empty pages', () => {
    expect(parseLinkEntities('<html></html>')).toEqual([])
  })
})
