/**
 * Behavior-pinning tests for downloadAppUpdate (CL4) — written BEFORE extracting
 * the streamToFile helper so the security-critical invariants are locked at the
 * public boundary and must survive the refactor unchanged:
 *
 *   - untrusted initial URL refused (no request made)
 *   - per-hop redirect re-validation on BOTH the checksum fetch and the installer
 *   - missing (404) checksum refused; malformed checksum refused
 *   - SHA-256 verified against the published digest; mismatch deletes the partial
 *   - truncation (received < content-length) detected; partial deleted
 *   - idle timeout aborts a stalled download
 *   - cancel during the checksum phase and mid-download both settle as canceled
 *   - progress events reach the WebContents
 */
import { describe, it, expect, vi, beforeEach, afterEach, afterAll } from 'vitest'
import { EventEmitter } from 'node:events'
import { mkdtempSync, rmSync, existsSync, readFileSync } from 'node:fs'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
import { createHash } from 'node:crypto'

const TMP = mkdtempSync(join(tmpdir(), 'aerofetch-updater-test-'))

// --- electron + settings mocks ----------------------------------------------

class FakeResponse extends EventEmitter {
  constructor(
    public statusCode: number,
    public headers: Record<string, string | string[]> = {}
  ) {
    super()
  }
}

class FakeRequest extends EventEmitter {
  ended = false
  aborted = false
  redirectsFollowed = 0
  constructor(public url: string) {
    super()
  }
  end(): void {
    this.ended = true
  }
  abort(): void {
    this.aborted = true
  }
  followRedirect(): void {
    this.redirectsFollowed++
  }
}

type Script = (req: FakeRequest) => void
const requestScripts: Script[] = []
const madeRequests: FakeRequest[] = []

vi.mock('electron', () => ({
  app: { getPath: () => TMP, getVersion: () => '0.5.0' },
  net: {
    request: (opts: { url: string }) => {
      const req = new FakeRequest(opts.url)
      madeRequests.push(req)
      const script = requestScripts.shift()
      if (script) queueMicrotask(() => script(req))
      return req
    }
  },
  shell: {}
}))

import { downloadAppUpdate, cancelAppUpdate } from '../src/main/updater'
import type { WebContents } from 'electron'

// --- helpers ------------------------------------------------------------------

const HOST = 'https://gitea.netbird.zimspace.uk:5938'
const ASSET = `${HOST}/debont80/AeroFetch/releases/download/v9.9.9/AeroFetch-Setup-9.9.9.exe`
const INSTALLER = Buffer.from('FAKE-INSTALLER-BYTES-'.repeat(64))
const GOOD_SHA = createHash('sha256').update(INSTALLER).digest('hex')

function fakeWc(): { wc: WebContents; sends: unknown[] } {
  const sends: unknown[] = []
  const wc = {
    isDestroyed: () => false,
    send: (_ch: string, p: unknown) => {
      sends.push(p)
    }
  } as unknown as WebContents
  return { wc, sends }
}

/** Script the checksum (.sha256) fetch to answer 200 with the given text. */
function checksumOk(text: string): Script {
  return (req) => {
    const res = new FakeResponse(200)
    req.emit('response', res)
    res.emit('data', Buffer.from(text))
    res.emit('end')
  }
}

/** Wait for the fire-and-forget partial unlink to land. */
async function expectGone(path: string): Promise<void> {
  for (let i = 0; i < 20 && existsSync(path); i++) await new Promise((r) => setTimeout(r, 25))
  expect(existsSync(path)).toBe(false)
}

const installerPath = join(TMP, 'AeroFetch-Setup-9.9.9.exe')

beforeEach(() => {
  requestScripts.length = 0
  madeRequests.length = 0
})

afterEach(() => {
  vi.useRealTimers()
  try {
    rmSync(installerPath, { force: true })
  } catch {
    /* ignore */
  }
})

afterAll(() => {
  rmSync(TMP, { recursive: true, force: true })
})

// --- the pins -------------------------------------------------------------------

describe('downloadAppUpdate (CL4 behavior pins)', () => {
  it('refuses an untrusted initial URL without making any request', async () => {
    const { wc } = fakeWc()
    const r = await downloadAppUpdate(wc, 'https://evil.example/x.exe')
    expect(r.ok).toBe(false)
    expect(r.error).toMatch(/untrusted/i)
    expect(madeRequests.length).toBe(0)
  })

  it('downloads, verifies the SHA-256, and reports progress (happy path)', async () => {
    requestScripts.push(checksumOk(`${GOOD_SHA}  AeroFetch-Setup-9.9.9.exe\n`))
    requestScripts.push((req) => {
      const res = new FakeResponse(200, { 'content-length': String(INSTALLER.length) })
      req.emit('response', res)
      res.emit('data', INSTALLER.subarray(0, 512))
      res.emit('data', INSTALLER.subarray(512))
      res.emit('end')
    })
    const { wc, sends } = fakeWc()
    const r = await downloadAppUpdate(wc, ASSET)
    expect(r).toEqual({ ok: true, filePath: installerPath })
    expect(readFileSync(installerPath).equals(INSTALLER)).toBe(true)
    expect(sends.length).toBeGreaterThan(0)
    expect(sends.at(-1)).toMatchObject({ received: INSTALLER.length, total: INSTALLER.length })
  })

  it('rejects a checksum mismatch and deletes the partial', async () => {
    const wrong = 'a'.repeat(64)
    requestScripts.push(checksumOk(`${wrong}  AeroFetch-Setup-9.9.9.exe\n`))
    requestScripts.push((req) => {
      const res = new FakeResponse(200, { 'content-length': String(INSTALLER.length) })
      req.emit('response', res)
      res.emit('data', INSTALLER)
      res.emit('end')
    })
    const { wc } = fakeWc()
    const r = await downloadAppUpdate(wc, ASSET)
    expect(r.ok).toBe(false)
    expect(r.error).toMatch(/checksum/i)
    await expectGone(installerPath)
  })

  it('detects truncation against content-length and deletes the partial', async () => {
    requestScripts.push(checksumOk(`${GOOD_SHA}  AeroFetch-Setup-9.9.9.exe\n`))
    requestScripts.push((req) => {
      const res = new FakeResponse(200, { 'content-length': String(INSTALLER.length * 2) })
      req.emit('response', res)
      res.emit('data', INSTALLER) // half of what was promised
      res.emit('end')
    })
    const { wc } = fakeWc()
    const r = await downloadAppUpdate(wc, ASSET)
    expect(r.ok).toBe(false)
    expect(r.error).toMatch(/incomplete/i)
    await expectGone(installerPath)
  })

  it('refuses to follow an installer redirect off the trusted host', async () => {
    requestScripts.push(checksumOk(`${GOOD_SHA}  AeroFetch-Setup-9.9.9.exe\n`))
    requestScripts.push((req) => {
      req.emit('redirect', 302, 'GET', 'https://evil.example/attachments/x.exe')
    })
    const { wc } = fakeWc()
    const r = await downloadAppUpdate(wc, ASSET)
    expect(r.ok).toBe(false)
    expect(r.error).toMatch(/redirect.*untrusted/i)
    const installerReq = madeRequests[1]!
    expect(installerReq.redirectsFollowed).toBe(0)
    expect(installerReq.aborted).toBe(true)
  })

  it('follows an on-host redirect (per-hop re-validation passes)', async () => {
    requestScripts.push(checksumOk(`${GOOD_SHA}  AeroFetch-Setup-9.9.9.exe\n`))
    requestScripts.push((req) => {
      req.emit('redirect', 302, 'GET', `${HOST}/attachments/abc123`)
      const res = new FakeResponse(200, { 'content-length': String(INSTALLER.length) })
      req.emit('response', res)
      res.emit('data', INSTALLER)
      res.emit('end')
    })
    const { wc } = fakeWc()
    const r = await downloadAppUpdate(wc, ASSET)
    expect(r.ok).toBe(true)
    expect(madeRequests[1]!.redirectsFollowed).toBe(1)
  })

  it('refuses a checksum-fetch redirect off the trusted host', async () => {
    requestScripts.push((req) => {
      req.emit('redirect', 302, 'GET', 'https://evil.example/x.sha256')
    })
    const { wc } = fakeWc()
    const r = await downloadAppUpdate(wc, ASSET)
    expect(r.ok).toBe(false)
    expect(r.error).toMatch(/checksum/i)
    expect(madeRequests.length).toBe(1) // never reached the installer request
  })

  it('refuses to install when the release has no checksum attached (404)', async () => {
    requestScripts.push((req) => {
      req.emit('response', new FakeResponse(404))
    })
    const { wc } = fakeWc()
    const r = await downloadAppUpdate(wc, ASSET)
    expect(r.ok).toBe(false)
    expect(r.error).toMatch(/no checksum/i)
    expect(madeRequests.length).toBe(1)
  })

  it('refuses a malformed checksum file (no 64-hex digest)', async () => {
    requestScripts.push(checksumOk('not a digest at all\n'))
    const { wc } = fakeWc()
    const r = await downloadAppUpdate(wc, ASSET)
    expect(r.ok).toBe(false)
    expect(r.error).toMatch(/malformed/i)
    expect(madeRequests.length).toBe(1)
  })

  it('a cancel during the checksum phase settles as canceled before any installer request', async () => {
    requestScripts.push((req) => {
      // Cancel lands while the checksum fetch is in flight.
      cancelAppUpdate()
      const res = new FakeResponse(200)
      req.emit('response', res)
      res.emit('data', Buffer.from(`${GOOD_SHA}  AeroFetch-Setup-9.9.9.exe\n`))
      res.emit('end')
    })
    const { wc } = fakeWc()
    const r = await downloadAppUpdate(wc, ASSET)
    expect(r.ok).toBe(false)
    expect(r.error).toMatch(/canceled/i)
    expect(madeRequests.length).toBe(1)
  })

  it('a cancel mid-download aborts, settles as canceled, and deletes the partial', async () => {
    requestScripts.push(checksumOk(`${GOOD_SHA}  AeroFetch-Setup-9.9.9.exe\n`))
    requestScripts.push((req) => {
      const res = new FakeResponse(200, { 'content-length': String(INSTALLER.length) })
      req.emit('response', res)
      res.emit('data', INSTALLER.subarray(0, 512))
      cancelAppUpdate()
    })
    const { wc } = fakeWc()
    const r = await downloadAppUpdate(wc, ASSET)
    expect(r.ok).toBe(false)
    expect(r.error).toMatch(/canceled/i)
    expect(madeRequests[1]!.aborted).toBe(true)
    await expectGone(installerPath)
  })

  it('times out a stalled download and deletes the partial', async () => {
    vi.useFakeTimers()
    requestScripts.push(checksumOk(`${GOOD_SHA}  AeroFetch-Setup-9.9.9.exe\n`))
    requestScripts.push((req) => {
      const res = new FakeResponse(200, { 'content-length': String(INSTALLER.length) })
      req.emit('response', res)
      res.emit('data', INSTALLER.subarray(0, 512))
      // …and then nothing: the stream stalls.
    })
    const { wc } = fakeWc()
    const pending = downloadAppUpdate(wc, ASSET)
    await vi.advanceTimersByTimeAsync(61_000)
    const r = await pending
    expect(r.ok).toBe(false)
    expect(r.error).toMatch(/stalled/i)
    expect(madeRequests[1]!.aborted).toBe(true)
    vi.useRealTimers()
    await expectGone(installerPath)
  })

  it('fails on an HTTP error status from the installer download', async () => {
    requestScripts.push(checksumOk(`${GOOD_SHA}  AeroFetch-Setup-9.9.9.exe\n`))
    requestScripts.push((req) => {
      req.emit('response', new FakeResponse(503))
    })
    const { wc } = fakeWc()
    const r = await downloadAppUpdate(wc, ASSET)
    expect(r.ok).toBe(false)
    expect(r.error).toMatch(/503/)
  })

  it('matches the checksum line for THIS asset in a combined checksum file', async () => {
    const other = 'b'.repeat(64)
    requestScripts.push(
      checksumOk(`${other}  SomethingElse.exe\n${GOOD_SHA}  AeroFetch-Setup-9.9.9.exe\n`)
    )
    requestScripts.push((req) => {
      const res = new FakeResponse(200, { 'content-length': String(INSTALLER.length) })
      req.emit('response', res)
      res.emit('data', INSTALLER)
      res.emit('end')
    })
    const { wc } = fakeWc()
    const r = await downloadAppUpdate(wc, ASSET)
    expect(r.ok).toBe(true)
  })
})
