import { test, expect } from '@playwright/test'

// Smoke tests for AZ-456 e2e infrastructure (AC-1, AC-2, AC-5, AC-8).
// Every other test file under e2e/tests/ is owned by AZ-457..AZ-482; those
// tasks add the production-shaped assertions per group. This file MUST stay
// minimal so any flake here is unambiguously an infrastructure regression.
//
// AZ-498 update (2026-05-12):
//   - The classic/satellite map toggle was removed; the SPA now consumes
//     only `satellite-provider` tiles via `VITE_SATELLITE_TILE_URL`. The
//     tile-stub serves `/tiles/{z}/{x}/{y}` (no `.png` suffix) per
//     `_docs/02_document/contracts/satellite-provider/tiles.md`.
//   - The dead OSM/Esri entries in EXTERNAL_HOSTS are removed; the SPA can
//     no longer attempt those hosts. The OWM and unpkg defenses stay.

const EXTERNAL_HOSTS = [
  /api\.openweathermap\.org/,
  /unpkg\.com/,
]

test.describe('AZ-456 e2e infrastructure', () => {
  test.beforeEach(async ({ context }, testInfo) => {
    // AC-8: external-host firewall. The compose network is already isolated
    // from the internet; this route guard catches code paths that try to
    // reach an external host directly (and lets resilience tests flip it).
    const externalHits: string[] = []
    await context.route(/.*/, async (route) => {
      const url = route.request().url()
      if (EXTERNAL_HOSTS.some((re) => re.test(new URL(url).hostname))) {
        externalHits.push(url)
        await route.abort()
        return
      }
      await route.continue()
    })
    testInfo.attachments.push({ name: 'externalHits', contentType: 'application/json', body: Buffer.from('[]') })
    ;(testInfo as unknown as { __externalHits: string[] }).__externalHits = externalHits
  })

  test.afterEach(async ({}, testInfo) => {
    const externalHits = (testInfo as unknown as { __externalHits?: string[] }).__externalHits ?? []
    expect(externalHits, 'leaked external requests detected').toEqual([])
  })

  test('AC-1: SPA HTML is served from azaion-ui', async ({ page }) => {
    const response = await page.goto('/')
    expect(response?.ok()).toBeTruthy()
    const html = await page.content()
    expect(html).toMatch(/<html[\s>]/i)
  })

  test('AC-2: owm-stub returns the canned wind shape', async ({ request }) => {
    const res = await request.get('http://owm-stub:8081/data/2.5/weather?lat=0&lon=0&appid=test')
    expect(res.status()).toBe(200)
    const body = await res.json()
    expect(body.wind).toEqual({ speed: 5.0, deg: 270 })
  })

  test('AC-2: tile-stub serves /tiles/{z}/{x}/{y} as a JPEG (AZ-498 contract)', async ({ request }) => {
    const res = await request.get('http://tile-stub:8082/tiles/1/0/0')
    expect(res.status()).toBe(200)
    expect(res.headers()['content-type']).toBe('image/jpeg')
    // Cache-Control + ETag are part of the contract — assert they're present
    // so a future tile-stub regression that drops them is caught here.
    expect(res.headers()['cache-control']).toMatch(/max-age=/)
    expect(res.headers()['etag']).toBeTruthy()
    const body = await res.body()
    // The stub serves a tiny PNG byte sequence; assert the PNG signature so
    // we know SOME image came back even though the Content-Type header is
    // jpeg-shaped. Production satellite-provider returns real JPEG bytes.
    expect(body.subarray(0, 8)).toEqual(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))
  })

  test('AC-5: Playwright runs under the configured browser project', async ({ browserName }) => {
    expect(['chromium', 'firefox']).toContain(browserName)
  })

  test('AC-8: external host is blocked by the route guard', async ({ page }) => {
    const externalHits = (test.info() as unknown as { __externalHits?: string[] }).__externalHits ?? []
    const beforeCount = externalHits.length
    await page.goto('/')
    await page.evaluate(() =>
      fetch('https://api.openweathermap.org/data/2.5/weather?appid=leak').catch(() => null),
    )
    // The guard converts the request into an abort, so the leak is recorded
    // but no real request escapes. The afterEach assertion will fire next.
    expect(externalHits.length).toBeGreaterThan(beforeCount)
    // Reset so afterEach doesn't fail this specific test (the guard already
    // proved the assertion).
    externalHits.length = 0
  })
})
