// Point the DB at a closed port before requiring anything that builds the pool —
// these tests inject fake models, so no real query should ever run.
process.env.DB_HOST = '127.0.0.1'
process.env.DB_PORT = '59999'

const { test, after } = require('node:test')
const assert = require('node:assert/strict')

const pushDispatch = require('../src/utils/pushDispatch')
// fromShardEvent moved out of pushDispatch in PR 4: core publishes, the shard
// side resolves an event to a stream and an owner (MODULE_SYSTEM.md §1.8).
const db = require('../src/utils/db')

after(() => db.close())

// Run body with env keys set, then restore prior values.
function withEnv(vars, fn) {
  const prior = {}
  for (const [k, v] of Object.entries(vars)) {
    prior[k] = process.env[k]
    if (v === undefined) delete process.env[k]
    else process.env[k] = v
  }
  try {
    return fn()
  } finally {
    for (const [k, v] of Object.entries(prior)) {
      if (v === undefined) delete process.env[k]
      else process.env[k] = v
    }
  }
}

// `mapShardEvent` and `fromShardEvent` left with module-uo in Phase 3: which
// shard event becomes which stream is the module's catalog, not core's
// infrastructure. What stays here is what core owns — the SSRF guard on the
// configured ntfy endpoint, and `publish()` delivering a content-free tickle to
// the right subscribers.
// ── isAllowedEndpoint (SSRF guard) ──────────────────────────────────────────

test('isAllowedEndpoint pins the configured ntfy origin and rejects everything else', () => {
  withEnv({ NTFY_BASE_URL: 'https://ntfy.example.com', NTFY_ALLOWED_ORIGINS: undefined }, () => {
    assert.equal(pushDispatch.isAllowedEndpoint('https://ntfy.example.com/UPabc'), true)
    assert.equal(pushDispatch.isAllowedEndpoint('https://evil.example.com/x'), false) // wrong origin
    assert.equal(pushDispatch.isAllowedEndpoint('http://ntfy.example.com/x'), false) // not https
    assert.equal(pushDispatch.isAllowedEndpoint('https://127.0.0.1/x'), false) // loopback
    assert.equal(pushDispatch.isAllowedEndpoint('not a url'), false)
  })
})

test('isAllowedEndpoint (no allow-set) permits any public https host but blocks private/loopback/http', () => {
  withEnv({ NTFY_BASE_URL: undefined, NTFY_ALLOWED_ORIGINS: undefined }, () => {
    assert.equal(pushDispatch.isAllowedEndpoint('https://relay.somehost.net/UPabc'), true)
    assert.equal(pushDispatch.isAllowedEndpoint('http://relay.somehost.net/x'), false)
    assert.equal(pushDispatch.isAllowedEndpoint('https://10.0.0.5/x'), false)
    assert.equal(pushDispatch.isAllowedEndpoint('https://192.168.1.10/x'), false)
    assert.equal(pushDispatch.isAllowedEndpoint('https://localhost/x'), false)
  })
})

// ── publish ─────────────────────────────────────────────────────────────────

function captureFetch() {
  const calls = []
  return {
    calls,
    fetchImpl: async (url, opts) => {
      calls.push({ url, opts })
      return { ok: true, status: 200 }
    },
  }
}

test('publish sends a content-free tickle to public subscribers', async () => {
  await withEnv({ NTFY_BASE_URL: undefined, NTFY_ALLOWED_ORIGINS: undefined }, async () => {
    const { calls, fetchImpl } = captureFetch()
    const streamCalls = []
    const pushDevices = {
      endpointsForStream: async (s) => {
        streamCalls.push(s)
        return [{ endpoint: 'https://relay.test/UPa', transport: 'unifiedpush' }]
      },
      endpointsForUserStream: async () => [],
    }
    await pushDispatch.publish('news.post', { ref: '5' }, { pushDevices, fetchImpl })
    assert.deepEqual(streamCalls, ['news.post'])
    assert.equal(calls.length, 1)
    assert.equal(calls[0].url, 'https://relay.test/UPa')
    assert.equal(calls[0].opts.method, 'POST')
    assert.deepEqual(JSON.parse(calls[0].opts.body), { stream: 'news.post', ref: '5' })
  })
})

test('publish (personal) targets only the owner’s subscribed devices', async () => {
  await withEnv({ NTFY_BASE_URL: undefined, NTFY_ALLOWED_ORIGINS: undefined }, async () => {
    const { calls, fetchImpl } = captureFetch()
    const userStreamCalls = []
    const pushDevices = {
      endpointsForStream: async () => {
        throw new Error('public path must not be used for a personal publish')
      },
      endpointsForUserStream: async (userId, s) => {
        userStreamCalls.push([userId, s])
        return [{ endpoint: 'https://relay.test/UPb' }]
      },
    }
    await pushDispatch.publish('vendor.sale', { ref: '9', ownerUserId: 42 }, { pushDevices, fetchImpl })
    assert.deepEqual(userStreamCalls, [[42, 'vendor.sale']])
    assert.equal(calls.length, 1)
  })
})

test('publish skips endpoints that fail the SSRF guard', async () => {
  await withEnv({ NTFY_BASE_URL: undefined, NTFY_ALLOWED_ORIGINS: undefined }, async () => {
    const { calls, fetchImpl } = captureFetch()
    const pushDevices = {
      endpointsForStream: async () => [
        { endpoint: 'http://relay.test/insecure' }, // not https → skipped
        { endpoint: 'https://10.0.0.9/private' }, // private → skipped
        { endpoint: 'https://relay.test/ok' }, // delivered
      ],
      endpointsForUserStream: async () => [],
    }
    await pushDispatch.publish('server.status', { ref: 'down' }, { pushDevices, fetchImpl })
    assert.equal(calls.length, 1)
    assert.equal(calls[0].url, 'https://relay.test/ok')
  })
})
