import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { http, HttpResponse } from 'msw'
import { act, useRef } from 'react'
import { server } from '../../tests/msw/server'
import { renderWithProviders, screen, waitFor } from '../../tests/helpers/render'
import { api, getToken, setToken } from '../api'
import { seedBearer, clearBearer } from '../../tests/helpers/auth'

// AZ-457 — Auth & token-handling at the React composition root.
//   FT-P-01  / row 02   — bootstrap refresh sends credentials:'include'
//                          (currently `quarantined` — bootstrap goes through
//                           api.get which doesn't thread credentials; row 02
//                           in results_report.md flags Step 4 fix pending)
//   FT-P-03  / row 11   — refresh transparency — children don't unmount;
//                          re-render delta ≤ 1
//   NFT-SEC-01 / row 04  — bearer never written to localStorage/sessionStorage
//                          (over the entire test lifetime, not just at one
//                           snapshot — AC-3)
//   NFT-SEC-02 / rows 05+ — refresh token not exposed via document.cookie at
//                          runtime (the static counterpart lives in
//                           scripts/run-tests.sh)
//
// Black-box discipline: the test imports the public AuthProvider component,
// the public setToken / getToken accessors, and React itself. It never
// reaches into AuthContext's internals (state setters, context value, etc.).
// All assertions are observable at the DOM, network, or storage surface.

interface StorageProbe {
  writes: Array<{ store: 'local' | 'session'; key: string; value: string }>
  values: Array<{ store: 'local' | 'session'; key: string; value: string }>
  cookieReads: number
  cookieWrites: string[]
  restore: () => void
}

function instrumentStorage(): StorageProbe {
  const probe: StorageProbe = {
    writes: [],
    values: [],
    cookieReads: 0,
    cookieWrites: [],
    restore: () => {
      /* installed below */
    },
  }
  const originalLocalSet = Storage.prototype.setItem
  const wrappedSet = function (this: Storage, key: string, value: string) {
    const store: 'local' | 'session' = this === window.localStorage ? 'local' : 'session'
    probe.writes.push({ store, key, value })
    return originalLocalSet.call(this, key, value)
  }
  Storage.prototype.setItem = wrappedSet

  // Patch document.cookie getter/setter on the document instance so reads /
  // writes surface in the probe without escaping jsdom's defaults.
  const cookieDescriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(document), 'cookie')
  if (cookieDescriptor) {
    Object.defineProperty(document, 'cookie', {
      configurable: true,
      get() {
        probe.cookieReads += 1
        return cookieDescriptor.get?.call(document) ?? ''
      },
      set(v: string) {
        probe.cookieWrites.push(v)
        cookieDescriptor.set?.call(document, v)
      },
    })
  }

  probe.restore = () => {
    Storage.prototype.setItem = originalLocalSet
    if (cookieDescriptor) {
      Object.defineProperty(document, 'cookie', cookieDescriptor)
    }
  }
  return probe
}

function snapshotAllStorageValues(): Array<{ store: 'local' | 'session'; key: string; value: string }> {
  const out: Array<{ store: 'local' | 'session'; key: string; value: string }> = []
  for (let i = 0; i < window.localStorage.length; i += 1) {
    const k = window.localStorage.key(i)!
    out.push({ store: 'local', key: k, value: window.localStorage.getItem(k) ?? '' })
  }
  for (let i = 0; i < window.sessionStorage.length; i += 1) {
    const k = window.sessionStorage.key(i)!
    out.push({ store: 'session', key: k, value: window.sessionStorage.getItem(k) ?? '' })
  }
  return out
}

describe('AZ-457 / src/auth/AuthContext.tsx — bootstrap, refresh, storage discipline', () => {
  let probe: StorageProbe

  beforeEach(() => {
    probe = instrumentStorage()
  })

  afterEach(() => {
    probe.restore()
    window.localStorage.clear()
    window.sessionStorage.clear()
    clearBearer()
  })

  describe('FT-P-01 (row 02) — bootstrap refresh', () => {
    it.fails('AuthProvider mount sends credentials:\'include\' on the bootstrap refresh (quarantined — Step 4 fix pending)', async () => {
      // Arrange — the production bootstrap path goes through `api.get(...)`,
      // which does NOT thread credentials. Row 02 in results_report.md is
      // `quarantined` until the bootstrap fetch is migrated to a path that
      // sets credentials:'include'. The inverted assertion below documents the
      // divergence next to its system-under-test; the day the production code
      // sends credentials:'include' on bootstrap, this test starts failing
      // and the it.fails wrapper is removed.
      let bootstrapCredentials: RequestCredentials | null = null
      server.use(
        http.get('/api/admin/auth/refresh', ({ request }) => {
          bootstrapCredentials = request.credentials
          return HttpResponse.json({
            user: { id: 'user-alice', email: 'op_alice@test.local', name: 'Alice', role: 'op', permissions: [] },
            token: 'bootstrap-bearer',
          })
        }),
      )

      // Act
      renderWithProviders(<div data-testid="app-root">app</div>)
      await waitFor(() => expect(bootstrapCredentials).not.toBeNull())

      // Assert — intentionally fails today.
      expect(bootstrapCredentials).toBe('include')
    })
  })

  describe('FT-P-03 (row 11) — refresh transparency: children stay mounted, re-render delta ≤ 1', () => {
    it('mid-session refresh does not unmount the protected child; re-render delta ≤ 1', async () => {
      // Arrange — a stable child component records its render counter.
      const renderTimes: number[] = []
      function StableChild() {
        const ref = useRef(0)
        ref.current += 1
        renderTimes.push(ref.current)
        return <div data-testid="stable-child">child #{ref.current}</div>
      }
      // Bootstrap returns a logged-in session (so the AuthProvider settles
      // immediately), then we trigger a 401-retry cycle on a downstream call.
      server.use(
        http.get('/api/admin/auth/refresh', () =>
          HttpResponse.json({
            user: { id: 'user-alice', email: 'op_alice@test.local', name: 'Alice', role: 'op', permissions: [] },
            token: 'bootstrap-bearer',
          }),
        ),
      )

      renderWithProviders(<StableChild />)
      await screen.findByTestId('stable-child')
      const renderCountAfterBootstrap = renderTimes.length

      // Force a 401-retry cycle on a downstream authed call.
      let firstHit = true
      let refreshHits = 0
      server.use(
        http.get('/api/admin/users/me', () => {
          if (firstHit) {
            firstHit = false
            return new HttpResponse(null, { status: 401 })
          }
          return HttpResponse.json({ id: 'user-alice', email: 'op_alice@test.local' })
        }),
        http.post('/api/admin/auth/refresh', () => {
          refreshHits += 1
          return HttpResponse.json({ token: 'rotated-bearer' })
        }),
      )

      // Act
      await act(async () => {
        await api.get('/api/admin/users/me')
      })

      // Assert — child stayed mounted (no unmount/remount); render delta ≤ 1.
      expect(screen.getByTestId('stable-child')).toBeInTheDocument()
      expect(refreshHits).toBe(1)
      const reRenderDelta = renderTimes.length - renderCountAfterBootstrap
      expect(reRenderDelta).toBeLessThanOrEqual(1) // row 11 — exact bound
    })
  })

  describe('NFT-SEC-01 (row 04) — bearer never in localStorage / sessionStorage', () => {
    it('over the entire test lifetime: no setItem call, no key/value contains the bearer', async () => {
      // Arrange — full bootstrap + refresh + downstream-authed call lifecycle.
      const BEARER = 'leak-trap-bearer-' + Date.now()
      let firstUsersMe = true
      server.use(
        http.get('/api/admin/auth/refresh', () =>
          HttpResponse.json({
            user: { id: 'user-alice', email: 'op_alice@test.local', name: 'Alice', role: 'op', permissions: [] },
            token: BEARER,
          }),
        ),
        http.post('/api/admin/auth/refresh', () => HttpResponse.json({ token: BEARER + '-rotated' })),
        http.get('/api/admin/users/me', () => {
          if (firstUsersMe) {
            firstUsersMe = false
            return new HttpResponse(null, { status: 401 })
          }
          return HttpResponse.json({ id: 'user-alice', email: 'op_alice@test.local' })
        }),
      )

      // Act — boot, then drive a refresh+retry cycle, then settle.
      renderWithProviders(<div data-testid="app">app</div>)
      await waitFor(() => expect(getToken()).toBe(BEARER))
      await act(async () => {
        await api.get('/api/admin/users/me')
      })
      await waitFor(() => expect(getToken()).toBe(BEARER + '-rotated'))

      // Assert — across the FULL test lifetime, no Storage.setItem call ever
      // referenced the bearer; no key in either store contains it (AC-3 says
      // "for the duration of the test", not just one snapshot).
      const writesContainingBearer = probe.writes.filter(
        (w) => w.value.includes(BEARER) || w.key.toLowerCase().includes('token') || w.key.toLowerCase().includes('bearer'),
      )
      expect(writesContainingBearer).toEqual([])

      const finalSnapshot = snapshotAllStorageValues()
      const leaked = finalSnapshot.filter((e) => e.value.includes(BEARER))
      expect(leaked).toEqual([])
    })
  })

  describe('NFT-SEC-02 (row 05) — refresh token not exposed via JS-readable document.cookie', () => {
    it('after bootstrap + refresh cycle, document.cookie carries no refresh token', async () => {
      // Arrange — drive a full auth lifecycle. If production code (or any
      // library it brings in) wrote a JS-readable cookie carrying token /
      // refresh material, it would surface in `document.cookie` here.
      // (HttpOnly cookies set by the real admin/ service are invisible to JS;
      // jsdom's MSW responses set no cookies at all unless the test does.)
      server.use(
        http.get('/api/admin/auth/refresh', () =>
          HttpResponse.json({
            user: { id: 'user-alice', email: 'op_alice@test.local', name: 'Alice', role: 'op', permissions: [] },
            token: 'bootstrap-bearer-XYZ',
          }),
        ),
        http.post('/api/admin/auth/refresh', () => HttpResponse.json({ token: 'rotated-bearer-ABC' })),
        http.get('/api/admin/users/me', () => HttpResponse.json({ id: 'user-alice', email: 'op_alice@test.local' })),
      )

      // Act — bootstrap + an authed call.
      renderWithProviders(<div data-testid="app">app</div>)
      await waitFor(() => expect(getToken()).toBe('bootstrap-bearer-XYZ'))
      await act(async () => {
        await api.get('/api/admin/users/me')
      })

      // Assert — JS-visible cookie jar carries neither bearer value nor any
      // refresh-prefixed cookie name (case-insensitive).
      const visibleCookies = document.cookie
      expect(visibleCookies, 'bearer must not appear in JS-readable cookies').not.toContain('bootstrap-bearer-XYZ')
      expect(visibleCookies, 'rotated bearer must not appear in JS-readable cookies').not.toContain('rotated-bearer-ABC')
      expect(visibleCookies, 'no refresh-named cookie should be JS-visible').not.toMatch(/refresh/i)

      // Defence-in-depth: production code did not write to document.cookie
      // during the cycle (any setter call would have surfaced here).
      expect(probe.cookieWrites).toEqual([])
    })
  })
})

// Type-only import guard — colocated test file does not import private
// AuthContext internals (only the public AuthProvider mount path, surfaced
// indirectly through renderWithProviders).
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type _PublicSurface = typeof setToken extends (t: string | null) => void ? true : never
