## tests/test_decentralized.nim
## Tests for Decentralized Architecture

import std/[unittest, tables, times, asyncdispatch, options, strutils]
import ../src/nimpak/[decentralized, merkle_tree, nippel_types]
import ../src/nimpak/utils/resultutils

suite "Decentralized Architecture":

  # ==========================================================================
  # Test: Peer-to-Peer Discovery
  # ==========================================================================

  test "Discovery manager creation":
    let manager = newDiscoveryManager(mDNS)
    check manager.protocol == mDNS
    check manager.localServices.len == 0
    check manager.discoveredServices.len == 0
    check manager.announceInterval == 30
    check not manager.isRunning

  test "Service announcement":
    let manager = newDiscoveryManager(mDNS)

    let announcement = ServiceAnnouncement(
      name: "test-nippel",
      kind: NippelService,
      port: 8080,
      utcpAddress: "utcp://localhost/nippel/test-nippel",
      metadata: initTable[string, string](),
      ttl: 300
    )

    proc testAnnounce() {.async.} =
      let result = await manager.announceService(announcement)
      check result.isOk
      check manager.localServices.hasKey("test-nippel")
      check manager.localServices["test-nippel"].kind == NippelService

    waitFor testAnnounce()

  test "Service discovery":
    let manager = newDiscoveryManager(mDNS)

    # Add a discovered service manually for testing
    let service = DiscoveredService(
      name: "remote-nippel",
      kind: NippelService,
      host: "192.168.1.100",
      port: 8080,
      protocol: mDNS,
      utcpAddress: "utcp://192.168.1.100/nippel/remote-nippel",
      metadata: initTable[string, string](),
      discoveredAt: now(),
      lastSeen: now()
    )

    manager.discoveredServices["remote-nippel"] = service

    proc testDiscover() {.async.} =
      let result = await manager.discoverServices(NippelService)
      check result.isOk
      let services = result.value
      check services.len == 1
      check services[0].name == "remote-nippel"

    waitFor testDiscover()

  test "Find service by name":
    let manager = newDiscoveryManager(mDNS)

    let service = DiscoveredService(
      name: "test-service",
      kind: CASService,
      host: "localhost",
      port: 9000,
      protocol: mDNS,
      utcpAddress: "utcp://localhost/cas/test-service",
      metadata: initTable[string, string](),
      discoveredAt: now(),
      lastSeen: now()
    )

    manager.discoveredServices["test-service"] = service

    let found = manager.findService("test-service")
    check found.isSome
    check found.get().kind == CASService

    let notFound = manager.findService("nonexistent")
    check notFound.isNone

  test "Update service last seen":
    let manager = newDiscoveryManager(mDNS)

    let service = DiscoveredService(
      name: "test-service",
      kind: NippelService,
      host: "localhost",
      port: 8080,
      protocol: mDNS,
      utcpAddress: "utcp://localhost/nippel/test-service",
      metadata: initTable[string, string](),
      discoveredAt: now() - 100.seconds,
      lastSeen: now() - 100.seconds
    )

    manager.discoveredServices["test-service"] = service

    let oldLastSeen = manager.discoveredServices["test-service"].lastSeen
    manager.updateServiceLastSeen("test-service")
    let newLastSeen = manager.discoveredServices["test-service"].lastSeen

    check newLastSeen > oldLastSeen

  test "Remove stale services":
    let manager = newDiscoveryManager(mDNS)

    # Add a fresh service
    let freshService = DiscoveredService(
      name: "fresh-service",
      kind: NippelService,
      host: "localhost",
      port: 8080,
      protocol: mDNS,
      utcpAddress: "utcp://localhost/nippel/fresh-service",
      metadata: initTable[string, string](),
      discoveredAt: now(),
      lastSeen: now()
    )

    # Add a stale service
    let staleService = DiscoveredService(
      name: "stale-service",
      kind: NippelService,
      host: "localhost",
      port: 8081,
      protocol: mDNS,
      utcpAddress: "utcp://localhost/nippel/stale-service",
      metadata: initTable[string, string](),
      discoveredAt: now() - 400.seconds,
      lastSeen: now() - 400.seconds
    )

    manager.discoveredServices["fresh-service"] = freshService
    manager.discoveredServices["stale-service"] = staleService

    check manager.discoveredServices.len == 2

    manager.removeStaleServices(300)  # Remove services older than 5 minutes

    check manager.discoveredServices.len == 1
    check manager.discoveredServices.hasKey("fresh-service")
    check not manager.discoveredServices.hasKey("stale-service")

  # ==========================================================================
  # Test: Distributed UTCP Addressing
  # ==========================================================================

  test "Parse distributed UTCP address":
    let result = parseDistributedUTCPAddress("utcp://example.com:8080/nippel/test")
    check result.isOk

    let address = result.value
    check address.scheme == "utcp"
    check address.host == "example.com"
    check address.port == 8080
    check address.resource == "/nippel/test"

  test "Parse UTCP address without port":
    let result = parseDistributedUTCPAddress("utcp://localhost/nexter/container1")
    check result.isOk

    let address = result.value
    check address.scheme == "utcp"
    check address.host == "localhost"
    check address.port == 8080  # Default port
    check address.resource == "/nexter/container1"

  test "Parse invalid UTCP address":
    let result = parseDistributedUTCPAddress("http://example.com/test")
    check result.isErr
    check result.error.contains("must start with utcp://")

  test "Format distributed UTCP address":
    let address = DistributedUTCPAddress(
      scheme: "utcp",
      host: "example.com",
      port: 9000,
      resource: "/nippel/test",
      query: initTable[string, string]()
    )

    let formatted = formatDistributedUTCPAddress(address)
    check formatted == "utcp://example.com:9000/nippel/test"

  test "Format UTCP address with default port":
    let address = DistributedUTCPAddress(
      scheme: "utcp",
      host: "localhost",
      port: 8080,
      resource: "/cas/store",
      query: initTable[string, string]()
    )

    let formatted = formatDistributedUTCPAddress(address)
    check formatted == "utcp://localhost/cas/store"

  test "UTCP router creation":
    let router = newUTCPRouter("localhost", 8080)
    check router.localAddress.host == "localhost"
    check router.localAddress.port == 8080
    check router.routes.len == 0
    check router.neighbors.len == 0

  test "Add route to router":
    let router = newUTCPRouter("localhost", 8080)

    let result = router.addRoute(
      "utcp://remote.host:9000/nippel/test",
      "192.168.1.100",
      metric = 5
    )

    check result.isOk
    check router.routes.len == 1

  test "Find route in router":
    let router = newUTCPRouter("localhost", 8080)

    discard router.addRoute(
      "utcp://remote.host:9000/nippel/test",
      "192.168.1.100",
      metric = 5
    )

    let found = router.findRoute("utcp://remote.host:9000/nippel/test")
    check found.isSome
    check found.get().nextHop == "192.168.1.100"
    check found.get().metric == 5

    let notFound = router.findRoute("utcp://nonexistent/test")
    check notFound.isNone

  test "Add neighbor to router":
    let router = newUTCPRouter("localhost", 8080)

    router.addNeighbor("192.168.1.100")
    router.addNeighbor("192.168.1.101")

    check router.neighbors.len == 2
    check "192.168.1.100" in router.neighbors
    check "192.168.1.101" in router.neighbors

    # Adding same neighbor again shouldn't duplicate
    router.addNeighbor("192.168.1.100")
    check router.neighbors.len == 2

  # ==========================================================================
  # Test: Merkle Tree Synchronization
  # ==========================================================================

  test "Sync manager creation":
    let manager = newSyncManager()
    check manager.activeSessions.len == 0
    check manager.syncInterval == 60

  test "Start sync session":
    let manager = newSyncManager()

    # Create a simple merkle tree for testing
    let tree = MerkleTree(
      root: MerkleNode(
        hash: "test-root-hash",
        path: "/",
        isLeaf: false,
        children: @[],
        size: 0
      ),
      hashAlgorithm: "xxh3",
      nodeCount: 1,
      leafCount: 0
    )

    proc testSync() {.async.} =
      let result = await manager.startSync(tree, "remote.host:8080")
      check result.isOk

      let sessionId = result.value
      check sessionId.len > 0
      check manager.activeSessions.hasKey(sessionId)

    waitFor testSync()

  test "Get sync status":
    let manager = newSyncManager()

    let tree = MerkleTree(
      root: MerkleNode(
        hash: "test-root-hash",
        path: "/",
        isLeaf: false,
        children: @[], size: 0
      ),
      hashAlgorithm: "xxh3", nodeCount: 1, leafCount: 0
    )

    proc testStatus() {.async.} =
      let result = await manager.startSync(tree, "remote.host:8080")
      check result.isOk

      let sessionId = result.value
      let status = manager.getSyncStatus(sessionId)

      check status.isSome
      check status.get().status == Syncing
      check status.get().remoteAddress == "remote.host:8080"

    waitFor testStatus()

  test "Cancel sync session":
    let manager = newSyncManager()

    let tree = MerkleTree(
      root: MerkleNode(
        hash: "test-root-hash",
        path: "/",
        isLeaf: false,
        children: @[], size: 0
      ),
      hashAlgorithm: "xxh3", nodeCount: 1, leafCount: 0
    )

    proc testCancel() {.async.} =
      let result = await manager.startSync(tree, "remote.host:8080")
      check result.isOk

      let sessionId = result.value
      check manager.activeSessions.hasKey(sessionId)

      let cancelResult = manager.cancelSync(sessionId)
      check cancelResult.isOk
      check not manager.activeSessions.hasKey(sessionId)

    waitFor testCancel()

  # ==========================================================================
  # Test: High-Level Operations
  # ==========================================================================

  test "Build decentralized cluster":
    proc testCluster() {.async.} =
      let result = await buildDecentralizedCluster(
        @["nippel1", "nippel2"],
        @["nexter1"]
      )

      check result.isOk
      let manager = result.value

      check manager.localServices.len == 3
      check manager.localServices.hasKey("nippel1")
      check manager.localServices.hasKey("nippel2")
      check manager.localServices.hasKey("nexter1")

    waitFor testCluster()

  # ==========================================================================
  # Test: String Conversions
  # ==========================================================================

  test "DiscoveryProtocol string conversion":
    check $mDNS == "mDNS"
    check $DNSSD == "DNS-SD"
    check $DHT == "DHT"
    check $Gossip == "Gossip"

  test "ServiceKind string conversion":
    check $NippelService == "Nippel"
    check $NexterService == "Nexter"
    check $CASService == "CAS"
    check $MerkleService == "Merkle"

  test "SyncStatus string conversion":
    check $InSync == "InSync"
    check $OutOfSync == "OutOfSync"
    check $Syncing == "Syncing"
    check $SyncFailed == "SyncFailed"

  test "DiffAction string conversion":
    check $Add == "Add"
    check $Remove == "Remove"
    check $Update == "Update"
    check $Conflict == "Conflict"

echo "✓ All Decentralized Architecture tests completed"
