import assert from "node:assert/strict"
import { rememberServerIfaces, resetIfaceCacheForTests } from "./traffic-flow-ifindex.js"
import {
  ingestParsedFlowsForServerForTests,
  resetFlowRingsForTests,
} from "./traffic-flow-ingest.js"
import { buildFlowAnalytics, formatLiveSseFromBuilder, getFlowMonthly, listFlowClients, listFlowExporters } from "./traffic-flow-analytics.js"
import { sqliteDatabase } from "../db/index.js"
import { seedFlowTopologyForTests, type FlowTopology } from "./traffic-flow-topology.js"
import { disableCatalogFetchForTests, resetFlowCatalogForTests, seedFlowCatalogForTests } from "./traffic-flow-classify.js"
import {
  disableRipeEnqueueForTests,
  disableRipePersistForTests,
  resetRipeCacheForTests,
  seedRipeCacheForTests,
} from "./traffic-flow-ripe.js"

disableCatalogFetchForTests()
resetFlowCatalogForTests()
disableRipePersistForTests()
resetRipeCacheForTests()
disableRipeEnqueueForTests()
resetIfaceCacheForTests()
resetFlowRingsForTests()
seedFlowTopologyForTests({
  clientIfaces: new Map(),
  clientByIface: new Map(),
  enNodes: [],
  enHosts: new Set(),
  jhHosts: new Set(),
  wanIfaces: new Map(),
  plane: { clientIfaceNames: new Set(), enHosts: new Set(), jhHosts: new Set() },
})
rememberServerIfaces(7, [
  { ".id": "*2", name: "ether1" },
  { ".id": "*B", name: "ether2" },
  { ".id": "*A", name: "wg-flow" },
])

ingestParsedFlowsForServerForTests(7, [
  {
    src: "10.1.1.8",
    dst: "8.8.8.8",
    proto: 6,
    srcPort: 51234,
    dstPort: 443,
    bytes: 12_000,
    packets: 10,
    inIface: "2",
    outIface: "11",
  },
  {
    src: "10.1.1.8",
    dst: "1.1.1.1",
    proto: 17,
    srcPort: 53000,
    dstPort: 53,
    bytes: 800,
    packets: 4,
    inIface: "2",
    outIface: "",
  },
])

try {
  const all = buildFlowAnalytics({ minutes: 5, serverId: 7 })
  assert.equal(all.applications[0]?.label, "HTTPS")
  assert.ok(all.protocols.some((p) => p.label === "TCP"))
  assert.equal(all.ifaces[0]?.name, "ether1")
  assert.notEqual(all.ifaces[0]?.name, "2")
  const conv = all.conversationsList[0]
  assert.ok(conv)
  assert.equal(conv.inIface, "ether1")
  assert.equal(conv.inIfaceIndex, "2")
  assert.equal(conv.application, "HTTPS")
  assert.ok(!/^\d+$/.test(conv.inIface))

  const filtered = buildFlowAnalytics({ minutes: 5, serverId: 7, iface: "ether1" })
  assert.ok(filtered.bytes >= 12_000)
  assert.equal(filtered.ifaces[0]?.name, "ether1")

  const miss = buildFlowAnalytics({ minutes: 5, serverId: 7, iface: "wg-flow" })
  assert.equal(miss.conversations, 0)

  const other = buildFlowAnalytics({ minutes: 5, serverId: 99 })
  assert.equal(other.conversations, 0)
} finally {
  resetFlowRingsForTests()
  resetIfaceCacheForTests()
}

resetFlowRingsForTests()
resetIfaceCacheForTests()
rememberServerIfaces(7, [
  { ".id": "*2", name: "ether1" },
  { ".id": "*B", name: "ether2" },
  { ".id": "*A", name: "wg-flow" },
])
ingestParsedFlowsForServerForTests(7, [
  {
    src: "10.1.1.8",
    dst: "8.8.8.8",
    proto: 6,
    srcPort: 51234,
    dstPort: 443,
    bytes: 12_000,
    packets: 10,
    inIface: "2",
    outIface: "11",
  },
  {
    src: "10.1.1.8",
    dst: "8.8.8.8",
    proto: 6,
    srcPort: 51234,
    dstPort: 443,
    bytes: 9_000,
    packets: 9,
    inIface: "11",
    outIface: "",
  },
])
try {
  const summed = buildFlowAnalytics({ minutes: 5, serverId: 7, dedup: false })
  assert.equal(summed.bytes, 21_000)
  assert.equal(summed.conversations, 2)
  const deduped = buildFlowAnalytics({ minutes: 5, serverId: 7, dedup: true })
  assert.equal(deduped.bytes, 12_000)
  assert.equal(deduped.conversations, 1)
  assert.equal(deduped.dedupApplied, true)
  assert.equal(deduped.interfaces.length, 2)
} finally {
  resetFlowRingsForTests()
  resetIfaceCacheForTests()
}

resetFlowRingsForTests()
resetIfaceCacheForTests()
disableRipeEnqueueForTests()
seedRipeCacheForTests({
  prefix: "8.8.8.0/24",
  asn: 15169,
  country: "US",
  lat: 37.4,
  lng: -122.1,
  holder: "GOOGLE",
  ok: true,
  fetchedAt: Date.now(),
})
seedFlowCatalogForTests({
  cidrs: [{ cidr: "8.8.8.0/24", purpose: "steam-gaming" }],
})
rememberServerIfaces(7, [{ ".id": "*2", name: "ether1" }])
ingestParsedFlowsForServerForTests(7, [
  {
    src: "10.1.1.8",
    dst: "8.8.8.8",
    proto: 6,
    srcPort: 51234,
    dstPort: 443,
    bytes: 12_000,
    packets: 10,
    inIface: "2",
    outIface: "",
  },
])
try {
  const geo = buildFlowAnalytics({ minutes: 5, serverId: 7 })
  assert.equal(geo.categories?.[0]?.label, "Игры")
  assert.ok(geo.asns?.some((r) => r.label.includes("AS15169")))
  assert.equal(geo.countries?.[0]?.id, "US")
  assert.equal(geo.mapEdges?.[0]?.toCountry, "US")
  assert.ok(geo.mapEdges?.every((e) => e.toCountry !== "?"))
  assert.equal(geo.conversationsList[0]?.dstCountry, "US")
  assert.equal(geo.asns?.[0]?.id, "15169")
} finally {
  resetFlowRingsForTests()
  resetIfaceCacheForTests()
  resetRipeCacheForTests()
  resetFlowCatalogForTests()
}

resetFlowRingsForTests()
resetIfaceCacheForTests()
disableRipeEnqueueForTests()
resetRipeCacheForTests()
resetFlowCatalogForTests()
seedRipeCacheForTests({
  prefix: "1.1.1.0/24",
  asn: 13335,
  country: "?",
  lat: null,
  lng: null,
  holder: "CLOUDFLARENET, US",
  ok: true,
  fetchedAt: Date.now(),
})
rememberServerIfaces(7, [{ ".id": "*2", name: "ether1" }])
ingestParsedFlowsForServerForTests(7, [
  {
    src: "10.1.1.8",
    dst: "1.1.1.1",
    proto: 6,
    srcPort: 51234,
    dstPort: 443,
    bytes: 5000,
    packets: 5,
    inIface: "2",
    outIface: "",
  },
])
try {
  const cf = buildFlowAnalytics({ minutes: 5, serverId: 7 })
  assert.equal(cf.countries?.[0]?.id, "US")
  assert.ok(cf.mapEdges?.every((e) => e.toCountry !== "?"))
  assert.equal(cf.services?.[0]?.label, "Cloudflare")
  assert.equal(cf.categories?.[0]?.label, "CDN")
  assert.equal(cf.conversationsList[0]?.dstCountry, "US")
  assert.equal(cf.asns?.[0]?.id, "13335")
} finally {
  resetFlowRingsForTests()
  resetIfaceCacheForTests()
  resetRipeCacheForTests()
  resetFlowCatalogForTests()
}

{
  ingestParsedFlowsForServerForTests(7, [
    {
      src: "10.1.1.8",
      dst: "8.8.8.8",
      proto: 6,
      srcPort: 51234,
      dstPort: 443,
      bytes: 12_000,
      packets: 10,
      inIface: "2",
      outIface: "",
    },
  ])
  const degraded = buildFlowAnalytics({ minutes: 5, serverId: 7, skipHeavy: true })
  assert.equal(degraded.degraded, true)
  assert.equal(degraded.conversationsList.length, 0)
  assert.ok((degraded.bytes ?? 0) >= 12_000)
  const liveErr = formatLiveSseFromBuilder(() => {
    throw new Error("SQLITE_BUSY")
  })
  assert.equal(liveErr.event, "error")
  assert.equal((liveErr.data as { error: string }).error, "SQLITE_BUSY")
  const liveOk = formatLiveSseFromBuilder(() => ({ ok: true }))
  assert.equal(liveOk.event, "sample")
  const exporters = listFlowExporters(5)
  const clients = listFlowClients(5)
  assert.ok(Array.isArray(exporters.exporters))
  assert.ok(Array.isArray(clients.clients))
  resetFlowRingsForTests()
}

{
  sqliteDatabase.prepare(`DELETE FROM flow_daily_dims WHERE server_id = 7 AND day LIKE '2026-09-%'`).run()
  sqliteDatabase.exec(`
    INSERT INTO flow_daily_dims (server_id, day, dim, key, bytes, packets)
    VALUES
      (7, '2026-09-01', 'country', 'US', 1000, 10),
      (7, '2026-09-02', 'country', 'US', 500, 5),
      (7, '2026-09-01', 'service', 'steam', 800, 8),
      (7, '2026-09-01', 'asn', '15169', 900, 9),
      (7, '2026-09-01', 'asn', 'other', 100, 1)
  `)
  const monthly = getFlowMonthly("2026-09", 7)
  assert.equal(monthly.bytes, 1500)
  assert.equal(monthly.countries[0]?.id, "US")
  assert.equal(monthly.countries[0]?.bytes, 1500)
  assert.ok(monthly.asns.some((row) => row.id === "other"))
  sqliteDatabase.prepare(`DELETE FROM flow_daily_dims WHERE server_id = 7 AND day LIKE '2026-09-%'`).run()
}

{
  resetFlowRingsForTests()
  resetIfaceCacheForTests()
  resetRipeCacheForTests()
  resetFlowCatalogForTests()
  disableRipeEnqueueForTests()
  rememberServerIfaces(7, [
    { ".id": "*2", name: "gre-client" },
    { ".id": "*3", name: "NSK-SERVHOST-RTK" },
    { ".id": "*4", name: "gre-en-nsk" },
  ])
  const topo: FlowTopology = {
    clientIfaces: new Map([[7, new Set(["gre-client"])]]),
    clientByIface: new Map([["7|gre-client", {
      userId: "u1",
      login: "alice",
      name: "Alice",
      serverId: 7,
      interfaceName: "gre-client",
    }]]),
    enNodes: [{ id: 9, name: "NSK-SERVHOST-RTK", hosts: ["198.51.100.1"] }],
    enHosts: new Set(["198.51.100.1"]),
    jhHosts: new Set(["203.0.113.10"]),
    wanIfaces: new Map(),
    plane: {
      clientIfaceNames: new Set(["gre-client"]),
      enHosts: new Set(["198.51.100.1"]),
      jhHosts: new Set(["203.0.113.10"]),
    },
  }
  seedFlowTopologyForTests(topo)
  seedRipeCacheForTests({
    prefix: "173.194.0.0/16",
    asn: 15169,
    country: "US",
    lat: null,
    lng: null,
    holder: "GOOGLE",
    ok: true,
    fetchedAt: Date.now(),
  })
  ingestParsedFlowsForServerForTests(7, [
    {
      src: "10.100.1.17",
      dst: "173.194.160.163",
      proto: 6,
      srcPort: 51234,
      dstPort: 443,
      bytes: 12_000,
      packets: 10,
      inIface: "2",
      outIface: "3",
      nextHop: "198.51.100.1",
    },
    {
      src: "203.0.113.10",
      dst: "198.51.100.1",
      proto: 47,
      srcPort: 0,
      dstPort: 0,
      bytes: 5_000_000,
      packets: 4000,
      inIface: "4",
      outIface: "4",
    },
    {
      src: "10.100.1.17",
      dst: "10.100.1.18",
      proto: 6,
      srcPort: 50000,
      dstPort: 443,
      bytes: 8000,
      packets: 8,
      inIface: "2",
      outIface: "2",
    },
  ])
  try {
    const def = buildFlowAnalytics({ minutes: 5, serverId: 7 })
    assert.equal(def.bytes, 12_000)
    assert.equal(def.bytesPayload, 12_000)
    assert.equal(def.bytesOverlay, 5_000_000)
    assert.equal(def.bytesMesh, 8000)
    assert.equal(def.excludeOverlayApplied, true)
    assert.equal(def.excludeMeshApplied, true)
    assert.ok(!def.conversationsList.some((r) => r.proto === 47))
    assert.equal(def.conversationsList[0]?.service, "Google")
    assert.equal(def.conversationsList[0]?.category, "Веб")
    assert.equal(def.conversationsList[0]?.clientName, "Alice")
    assert.equal(def.conversationsList[0]?.enName, "NSK-SERVHOST-RTK")
    assert.equal(def.conversationsList[0]?.plane, "payload")
    const path = def.paths?.[0]
    assert.ok(path)
    assert.equal(path.clientName, "Alice")
    assert.equal(path.enName, "NSK-SERVHOST-RTK")
    assert.equal(path.dst, "173.194.160.163")
    const withAll = buildFlowAnalytics({ minutes: 5, serverId: 7, excludeOverlay: false, excludeMesh: false })
    assert.equal(withAll.bytes, 12_000 + 5_000_000 + 8000)
    assert.ok(withAll.conversationsList.some((r) => r.plane === "overlay"))
    assert.ok(withAll.conversationsList.some((r) => r.plane === "client_mesh"))
  } finally {
    seedFlowTopologyForTests(null)
    resetFlowRingsForTests()
    resetIfaceCacheForTests()
    resetRipeCacheForTests()
    resetFlowCatalogForTests()
  }
}

{
  resetFlowRingsForTests()
  resetIfaceCacheForTests()
  resetRipeCacheForTests()
  disableRipeEnqueueForTests()
  rememberServerIfaces(7, [{ ".id": "*2", name: "ether1" }])
  ingestParsedFlowsForServerForTests(7, [
    {
      src: "173.194.151.65",
      dst: "10.200.100.53",
      proto: 6,
      srcPort: 443,
      dstPort: 57182,
      bytes: 12_000,
      packets: 10,
      inIface: "2",
      outIface: "2",
    },
    {
      src: "104.18.35.51",
      dst: "10.200.100.53",
      proto: 6,
      srcPort: 443,
      dstPort: 53880,
      bytes: 3_000,
      packets: 4,
      inIface: "2",
      outIface: "2",
    },
  ])
  try {
    const rev = buildFlowAnalytics({ minutes: 5, serverId: 7 })
    const google = rev.conversationsList.find((r) => r.src === "173.194.151.65")
    const cf = rev.conversationsList.find((r) => r.src === "104.18.35.51")
    assert.equal(google?.service, "Google")
    assert.equal(google?.category, "Веб")
    assert.equal(cf?.service, "Cloudflare")
    assert.equal(cf?.category, "CDN")
  } finally {
    resetFlowRingsForTests()
    resetIfaceCacheForTests()
    resetRipeCacheForTests()
  }
}

{
  resetFlowRingsForTests()
  resetIfaceCacheForTests()
  const sidRow = sqliteDatabase.prepare(`SELECT id FROM servers LIMIT 1`).get() as { id?: number } | undefined
  if (sidRow?.id) {
    const sid = sidRow.id
    rememberServerIfaces(sid, [{ ".id": "*4", name: "gre-en-nsk" }])
    ingestParsedFlowsForServerForTests(sid, [{
      src: "10.100.1.17",
      dst: "8.8.8.8",
      proto: 6,
      srcPort: 1,
      dstPort: 443,
      bytes: 100,
      packets: 1,
      inIface: "4",
      outIface: "4",
    }])
    sqliteDatabase.prepare(`
      INSERT INTO traffic_samples (server_id, interface_name, sampled_at, rx_bytes, tx_bytes, rx_bps, tx_bps)
      VALUES (?, 'gre-en-nsk', datetime('now'), 9000000, 1000000, 40000000, 2000000)
    `).run(sid)
    try {
      const wire = buildFlowAnalytics({ minutes: 5, serverId: sid })
      assert.ok((wire.bpsWire ?? 0) >= 40_000_000)
      assert.notEqual(wire.bpsWire, (wire.bytes * 8) / 300)
    } finally {
      sqliteDatabase.prepare(`DELETE FROM traffic_samples WHERE server_id = ? AND interface_name = 'gre-en-nsk'`).run(sid)
    }
  }
  resetFlowRingsForTests()
  resetIfaceCacheForTests()
}

console.log("traffic-flow-analytics.test.ts: ok")
