package events_test

import (
	"testing"

	edgeevents "iop/apps/edge/internal/events"
	iop "iop/proto/gen/iop"
)

func TestBusPublishesRunToSpecificAndAllSubscribers(t *testing.T) {
	bus := edgeevents.NewBus()
	specific, unsubscribeSpecific := bus.SubscribeRun("run-1", 1)
	defer unsubscribeSpecific()
	all, unsubscribeAll := bus.SubscribeAllRuns(1)
	defer unsubscribeAll()

	bus.PublishRun(&iop.RunEvent{RunId: "run-1", Type: "start"})

	select {
	case event := <-specific:
		if event.GetRunId() != "run-1" {
			t.Fatalf("specific run id: %q", event.GetRunId())
		}
	default:
		t.Fatal("expected specific run subscriber event")
	}
	select {
	case event := <-all:
		if event.GetRunId() != "run-1" {
			t.Fatalf("all run id: %q", event.GetRunId())
		}
	default:
		t.Fatal("expected all run subscriber event")
	}
}

func TestBusCountsDroppedRunEvents(t *testing.T) {
	bus := edgeevents.NewBus()
	_, unsubscribe := bus.SubscribeRun("run-1", 1)
	defer unsubscribe()

	// First event fills the buffer; subsequent ones are dropped until drained.
	bus.PublishRun(&iop.RunEvent{RunId: "run-1", Type: "start"})
	bus.PublishRun(&iop.RunEvent{RunId: "run-1", Type: "chunk-1"})
	bus.PublishRun(&iop.RunEvent{RunId: "run-1", Type: "chunk-2"})

	stats := bus.Stats()
	if stats.DroppedRunEvents != 2 {
		t.Fatalf("DroppedRunEvents: got %d want 2", stats.DroppedRunEvents)
	}
	if stats.DroppedNodeEvents != 0 {
		t.Fatalf("DroppedNodeEvents: got %d want 0", stats.DroppedNodeEvents)
	}
}

func TestBusCountsDroppedNodeEvents(t *testing.T) {
	bus := edgeevents.NewBus()
	_, unsubscribe := bus.SubscribeNode("node-1", 1)
	defer unsubscribe()

	bus.PublishNode(&iop.EdgeNodeEvent{NodeId: "node-1", Type: "node.connected"})
	bus.PublishNode(&iop.EdgeNodeEvent{NodeId: "node-1", Type: "node.disconnected"})
	bus.PublishNode(&iop.EdgeNodeEvent{NodeId: "node-1", Type: "node.reconnected"})

	stats := bus.Stats()
	if stats.DroppedNodeEvents != 2 {
		t.Fatalf("DroppedNodeEvents: got %d want 2", stats.DroppedNodeEvents)
	}
	if stats.DroppedRunEvents != 0 {
		t.Fatalf("DroppedRunEvents: got %d want 0", stats.DroppedRunEvents)
	}
}

func TestBusReplaysRecentRunEvents(t *testing.T) {
	bus := edgeevents.NewBus()

	bus.PublishRun(&iop.RunEvent{RunId: "run-1", Type: "start"})
	bus.PublishRun(&iop.RunEvent{RunId: "run-2", Type: "start"})
	bus.PublishRun(&iop.RunEvent{RunId: "run-1", Type: "complete"})

	replay := bus.ReplayRun("run-1")
	if len(replay) != 2 {
		t.Fatalf("ReplayRun len: got %d want 2", len(replay))
	}
	if replay[0].GetType() != "start" || replay[1].GetType() != "complete" {
		t.Fatalf("replay order: got [%q,%q] want [start,complete]", replay[0].GetType(), replay[1].GetType())
	}

	if got := bus.ReplayRun("missing"); got != nil {
		t.Fatalf("expected nil replay for unknown run, got %v", got)
	}
}

func TestBusReplayIsBounded(t *testing.T) {
	const cap = 3
	bus := edgeevents.NewBusWithReplayCapacity(cap)

	for i := 0; i < cap+5; i++ {
		bus.PublishRun(&iop.RunEvent{RunId: "run-1", Type: "tick"})
		bus.PublishNode(&iop.EdgeNodeEvent{NodeId: "node-1", Type: "tick"})
	}

	if got := bus.ReplayRun("run-1"); len(got) != cap {
		t.Fatalf("ReplayRun len: got %d want %d", len(got), cap)
	}
	if got := bus.ReplayNode("node-1"); len(got) != cap {
		t.Fatalf("ReplayNode len: got %d want %d", len(got), cap)
	}
}

func TestBusPublishesNodeToSpecificAndAllSubscribers(t *testing.T) {
	bus := edgeevents.NewBus()
	specific, unsubscribeSpecific := bus.SubscribeNode("node-1", 1)
	defer unsubscribeSpecific()
	all, unsubscribeAll := bus.SubscribeAllNodes(1)
	defer unsubscribeAll()

	bus.PublishNode(&iop.EdgeNodeEvent{NodeId: "node-1", Type: "node.disconnected"})

	select {
	case event := <-specific:
		if event.GetNodeId() != "node-1" {
			t.Fatalf("specific node id: %q", event.GetNodeId())
		}
	default:
		t.Fatal("expected specific node subscriber event")
	}
	select {
	case event := <-all:
		if event.GetNodeId() != "node-1" {
			t.Fatalf("all node id: %q", event.GetNodeId())
		}
	default:
		t.Fatal("expected all node subscriber event")
	}
}
