import { afterEach, describe, expect, it, vi } from 'vitest'
import { readConfig } from '../src/config'
import { createHostPinStore } from '../src/host-pin-store'
import { createPassthroughTransport, type WebSocketLike } from '../src/ws-transport'
import { mountTerminalView, type TerminalLike } from '../src/terminal-view'
import { decodeServerMessage } from '../src/protocol'
import { createWebAuthnClient } from '../src/webauthn'
import { mountPreviewClient, type ReadonlyTerminalLike } from '../src/preview-client'
import { createApiClient } from '../src/api-client'
import { mountPasswordLogin } from '../src/login-password'
import type { TerminalTransport } from '../src/ws-transport'

const cfg = readConfig({ protocol: 'http:', host: 'a.term.localhost', hostname: 'a.term.localhost' })

afterEach(() => vi.unstubAllGlobals())

describe('host-pin-store fallbacks', () => {
  it('uses an in-memory Storage when localStorage is unavailable', () => {
    vi.stubGlobal('localStorage', undefined)
    const pins = createHostPinStore()
    expect(pins.get('h')).toBeNull()
    pins.record('h', 'fpr-mem')
    expect(pins.get('h')).toBe('fpr-mem')
  })
})

describe('ws-transport defensive branches', () => {
  it('send() before open() throws (never silently drops bytes)', () => {
    const t = createPassthroughTransport(cfg, { wsCtor: FakeWs })
    expect(() => t.send(new Uint8Array([1]))).toThrow('transport not open')
  })

  it('decodes a string WS payload to bytes', async () => {
    const t = createPassthroughTransport(cfg, { wsCtor: FakeWs })
    const got: Uint8Array[] = []
    t.onMessage((b) => got.push(b))
    const opened = t.open()
    FakeWs.last!.fire()
    await opened
    FakeWs.last!.msg('hi')
    expect(new TextDecoder().decode(got[0]!)).toBe('hi')
    t.close()
  })
})

describe('terminal-view defensive branches', () => {
  it('drops a send when the transport is not open (caught), and disposes cleanly', () => {
    const root = document.createElement('div')
    Object.defineProperty(root, 'clientWidth', { value: 10 })
    Object.defineProperty(root, 'clientHeight', { value: 10 })
    const throwing: TerminalTransport = {
      open: async () => {},
      send: () => {
        throw new Error('not open')
      },
      onMessage: () => {},
      onClose: () => {},
      close: () => {},
    }
    const term: TerminalLike = {
      cols: 80,
      rows: 24,
      open: () => {},
      write: () => {},
      onData: () => {},
      fit: () => {},
      dispose: () => {},
    }
    const view = mountTerminalView(root, throwing, { createTerminal: () => term })
    view.dispose()
  })
})

describe('protocol edge cases', () => {
  it('decodes exit with a reason and rejects unknown/array frames', () => {
    expect(decodeServerMessage(enc({ type: 'exit', code: 1, reason: 'bye' }))).toEqual({
      type: 'exit',
      code: 1,
      reason: 'bye',
    })
    expect(decodeServerMessage(enc({ type: 'nope' }))).toBeNull()
    expect(decodeServerMessage(enc([1, 2]))).toBeNull()
  })
})

describe('webauthn register mapping', () => {
  it('maps an attestation credential to base64url fields', async () => {
    const attestation = {
      id: 'reg',
      type: 'public-key',
      rawId: new Uint8Array([1]).buffer,
      response: {
        clientDataJSON: new Uint8Array([2]).buffer,
        attestationObject: new Uint8Array([3]).buffer,
      },
    }
    const container = {
      create: vi.fn(async () => attestation),
      get: vi.fn(),
    } as unknown as CredentialsContainer
    const res = await createWebAuthnClient(container).register(
      {} as PublicKeyCredentialCreationOptions,
    )
    expect(res.response.attestationObject.length).toBeGreaterThan(0)
    expect(res.id).toBe('reg')
  })
})

describe('webauthn error branches', () => {
  it('register cancel → cancelled; register/authenticate null → failed; AbortError → cancelled', async () => {
    const cancel = {
      create: vi.fn(async () => {
        throw Object.assign(new Error('abort'), { name: 'AbortError' })
      }),
      get: vi.fn(async () => null),
    } as unknown as CredentialsContainer
    const wa = createWebAuthnClient(cancel)
    await expect(wa.register({} as PublicKeyCredentialCreationOptions)).rejects.toMatchObject({
      kind: 'cancelled',
    })
    await expect(
      wa.authenticate({} as PublicKeyCredentialRequestOptions),
    ).rejects.toMatchObject({ kind: 'failed' }) // null assertion → failed

    const nullReg = {
      create: vi.fn(async () => null),
      get: vi.fn(),
    } as unknown as CredentialsContainer
    await expect(
      createWebAuthnClient(nullReg).register({} as PublicKeyCredentialCreationOptions),
    ).rejects.toMatchObject({ kind: 'failed' })
  })
})

describe('webauthn non-Error rejection', () => {
  it('maps a thrown non-Error value to WebAuthnError("failed")', async () => {
    const container = {
      get: vi.fn(async () => {
        throw 'string failure' // eslint-disable-line no-throw-literal
      }),
      create: vi.fn(),
    } as unknown as CredentialsContainer
    await expect(
      createWebAuthnClient(container).authenticate({} as PublicKeyCredentialRequestOptions),
    ).rejects.toMatchObject({ kind: 'failed' })
  })
})

describe('dashboard optional-branch coverage', () => {
  it('renders with default pollMs and an open action that is a no-op when onOpen is absent', async () => {
    const { mountDashboard } = await import('../src/dashboard')
    const root = document.createElement('div')
    const api = {
      listHosts: vi.fn(async () => [
        {
          hostId: 'id-a',
          accountId: 'acc',
          subdomain: 'a',
          agentPubkey: new Uint8Array([1]),
          enrollFpr: 'f',
          status: 'online',
          lastSeen: 't',
          createdAt: 't',
          revokedAt: null,
        },
      ]),
    } as unknown as import('../src/api-client').ApiClient
    const dash = mountDashboard(root, api) // no opts → default pollMs, onOpen undefined
    await dash.refresh()
    expect(() => (root.querySelector('.host-open') as HTMLButtonElement).click()).not.toThrow()
    dash.dispose()
  })
})

describe('dashboard generic-error branch', () => {
  it('a non-ApiError rejection still shows the generic banner and keeps polling', async () => {
    const { mountDashboard } = await import('../src/dashboard')
    const root = document.createElement('div')
    const api = {
      listHosts: vi.fn(async () => {
        throw new Error('kaboom')
      }),
    } as unknown as import('../src/api-client').ApiClient
    const dash = mountDashboard(root, api, { pollMs: 100000 })
    await dash.refresh()
    expect(root.querySelector('.dashboard-error')?.textContent).toBe('Could not load hosts.')
    dash.dispose()
  })
})

describe('ws-transport raw Uint8Array message branch', () => {
  it('delivers a raw Uint8Array payload unchanged', async () => {
    const t = createPassthroughTransport(cfg, { wsCtor: FakeWs })
    const got: Uint8Array[] = []
    t.onMessage((b) => got.push(b))
    const opened = t.open()
    FakeWs.last!.fire()
    await opened
    FakeWs.last!.msg(new Uint8Array([7, 7]))
    expect(got[0]).toEqual(new Uint8Array([7, 7]))
    t.close()
  })
})

describe('preview-client unavailable path', () => {
  it('shows "unavailable" when key derivation throws', async () => {
    const card = document.createElement('div')
    const client = mountPreviewClient(
      card,
      { sessionId: 's', alg: 'aes-256-gcm', epoch: 'gen-1', frames: [new Uint8Array([1])] },
      new Uint8Array(32),
      { cols: 80, rows: 24 },
      {
        deriveContentKey: () => {
          throw new Error('bad key')
        },
        openReplayCiphertext: () => new Uint8Array(),
        createTerminal: () => ({ open: () => {}, write: () => {}, dispose: () => {} }) as ReadonlyTerminalLike,
      },
    )
    await client.render()
    expect(card.querySelector('.preview-unavailable')?.textContent).toBe('unavailable')
    client.dispose()
  })
})

describe('api-client + login-password error branches', () => {
  it('hostStatus returns the parsed status; a network error maps to ApiError(network)', async () => {
    const ok = vi.fn(async () => ({ ok: true, status: 200, json: async () => ({ status: 'online' }) }) as Response)
    const api = createApiClient(cfg, ok as unknown as typeof fetch)
    expect(await api.hostStatus('h1')).toBe('online')

    const boom = vi.fn(async () => {
      throw new Error('offline')
    })
    await expect(
      createApiClient(cfg, boom as unknown as typeof fetch).listHosts(),
    ).rejects.toMatchObject({ kind: 'network' })
  })

  it('password login surfaces a network error via textContent', async () => {
    const root = document.createElement('div')
    const boom = vi.fn(async () => {
      throw new Error('down')
    }) as unknown as typeof fetch
    const login = mountPasswordLogin(root, cfg, { fetchImpl: boom })
    await expect(login.submit('tok')).resolves.toBe('rejected')
    expect(root.querySelector('.login-error')?.textContent).toContain('Network error')
  })
})

// ── helpers ──
function enc(v: unknown): Uint8Array {
  return new TextEncoder().encode(JSON.stringify(v))
}

class FakeWs implements WebSocketLike {
  static last: FakeWs | null = null
  binaryType = ''
  protocol = ''
  onopen: ((ev: unknown) => void) | null = null
  onmessage: ((ev: { data: unknown }) => void) | null = null
  onclose: ((ev: { code?: number; reason?: string }) => void) | null = null
  onerror: ((ev: unknown) => void) | null = null
  constructor() {
    FakeWs.last = this
  }
  send(): void {}
  close(): void {}
  fire(): void {
    this.protocol = 'term.relay.v1'
    this.onopen?.({})
  }
  msg(data: unknown): void {
    this.onmessage?.({ data })
  }
}
