// Copyright 2026 Beacon Contributors
// SPDX-License-Identifier: AGPL-3.0-or-later

package db

import (
	"context"
	"errors"
	"testing"
	"time"

	sqlc "github.com/MeshCore-Beacon/beacon-server/db/sqlc"
	mockdb "github.com/MeshCore-Beacon/beacon-server/db/sqlc/mock"
	"github.com/MeshCore-Beacon/beacon-server/internal/ingest"
	"github.com/google/uuid"
	"github.com/jackc/pgx/v5/pgtype"
	"go.uber.org/mock/gomock"
)

func TestUpsertNode_WithRadio(t *testing.T) {
	ctrl := gomock.NewController(t)
	mock := mockdb.NewMockQuerier(ctrl)

	nodeID := uuid.MustParse("00000000-0000-0000-0000-000000000001")
	freq := float32(915.0)
	sf := int16(7)
	bw := float32(125.0)

	mock.EXPECT().
		UpsertNode(gomock.Any(), gomock.Any()).
		Return(sqlc.Node{ID: nodeID}, nil)

	store := &Store{q: mock}
	id, err := store.UpsertNode(context.Background(), ingest.UpsertNodeParams{
		PublicKey: []byte{0x01},
		NodeType:  1,
		Name:      "test-node",
	}, ingest.RadioSettings{FreqMHz: freq, SF: sf, BWKHz: bw})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if id != nodeID {
		t.Errorf("expected ID %s, got %s", nodeID, id)
	}
}

func TestUpsertNode_ComputesClockDrift(t *testing.T) {
	ctrl := gomock.NewController(t)
	mock := mockdb.NewMockQuerier(ctrl)

	nodeID := uuid.MustParse("00000000-0000-0000-0000-000000000001")
	deviceTime := uint32(time.Now().Add(-10 * time.Minute).Unix())

	var captured sqlc.UpsertNodeParams
	mock.EXPECT().
		UpsertNode(gomock.Any(), gomock.Any()).
		DoAndReturn(func(_ context.Context, p sqlc.UpsertNodeParams) (sqlc.Node, error) {
			captured = p
			return sqlc.Node{ID: nodeID}, nil
		})

	store := &Store{q: mock}
	_, err := store.UpsertNode(context.Background(), ingest.UpsertNodeParams{
		PublicKey:       []byte{0x01},
		NodeType:        2, // repeater
		Name:            "test-repeater",
		AdvertTimestamp: deviceTime,
	}, ingest.RadioSettings{})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if captured.DeviceClockDriftSeconds == nil {
		t.Fatal("expected DeviceClockDriftSeconds to be set")
	}
	// device clock is ~10 minutes (600s) behind; allow a few seconds of test-runtime slop
	got := *captured.DeviceClockDriftSeconds
	if got > -595 || got < -605 {
		t.Errorf("expected drift near -600s, got %d", got)
	}
}

func TestUpsertNode_NoAdvertTimestamp_OmitsDrift(t *testing.T) {
	ctrl := gomock.NewController(t)
	mock := mockdb.NewMockQuerier(ctrl)

	nodeID := uuid.MustParse("00000000-0000-0000-0000-000000000001")

	var captured sqlc.UpsertNodeParams
	mock.EXPECT().
		UpsertNode(gomock.Any(), gomock.Any()).
		DoAndReturn(func(_ context.Context, p sqlc.UpsertNodeParams) (sqlc.Node, error) {
			captured = p
			return sqlc.Node{ID: nodeID}, nil
		})

	store := &Store{q: mock}
	_, err := store.UpsertNode(context.Background(), ingest.UpsertNodeParams{
		PublicKey: []byte{0x01},
		NodeType:  2, // repeater, but AdvertTimestamp left zero (e.g. decode failed upstream)
		Name:      "test-repeater",
	}, ingest.RadioSettings{})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if captured.DeviceClockDriftSeconds != nil {
		t.Error("expected DeviceClockDriftSeconds to remain nil when AdvertTimestamp is zero")
	}
}

func TestUpsertNode_WithoutRadio(t *testing.T) {
	ctrl := gomock.NewController(t)
	mock := mockdb.NewMockQuerier(ctrl)

	nodeID := uuid.MustParse("00000000-0000-0000-0000-000000000001")

	mock.EXPECT().
		UpsertNode(gomock.Any(), gomock.Any()).
		Return(sqlc.Node{ID: nodeID}, nil)

	store := &Store{q: mock}
	id, err := store.UpsertNode(context.Background(), ingest.UpsertNodeParams{
		PublicKey: []byte{0x01},
		NodeType:  1,
		Name:      "test-node",
	}, ingest.RadioSettings{})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if id != nodeID {
		t.Errorf("expected ID %s, got %s", nodeID, id)
	}
}

func TestSetNodeCapability_BothTrue(t *testing.T) {
	ctrl := gomock.NewController(t)
	mock := mockdb.NewMockQuerier(ctrl)

	nodeID := uuid.MustParse("00000000-0000-0000-0000-000000000001")

	mock.EXPECT().SetNodeMultibytePaths(gomock.Any(), nodeID).Return(nil)
	mock.EXPECT().SetNodeMultibyteTraces(gomock.Any(), nodeID).Return(nil)

	store := &Store{q: mock}
	err := store.SetNodeCapability(context.Background(), nodeID, true, true)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestSetNodeCapability_PathsOnly(t *testing.T) {
	ctrl := gomock.NewController(t)
	mock := mockdb.NewMockQuerier(ctrl)

	nodeID := uuid.MustParse("00000000-0000-0000-0000-000000000001")

	mock.EXPECT().SetNodeMultibytePaths(gomock.Any(), nodeID).Return(nil)

	store := &Store{q: mock}
	err := store.SetNodeCapability(context.Background(), nodeID, true, false)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestSetNodeCapability_NeitherSet(t *testing.T) {
	ctrl := gomock.NewController(t)
	mock := mockdb.NewMockQuerier(ctrl)

	nodeID := uuid.MustParse("00000000-0000-0000-0000-000000000001")

	// no EXPECT — neither sqlc method should be called
	store := &Store{q: mock}
	err := store.SetNodeCapability(context.Background(), nodeID, false, false)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
}

func TestListNodes_Pagination(t *testing.T) {
	ctrl := gomock.NewController(t)
	mock := mockdb.NewMockQuerier(ctrl)

	nodeID := uuid.MustParse("00000000-0000-0000-0000-000000000001")
	lastSeen := pgtype.Timestamptz{Time: time.UnixMilli(1700000000000), Valid: true}

	rows := make([]sqlc.ListNodesRow, 3)
	for i := range rows {
		rows[i] = sqlc.ListNodesRow{
			ID:        nodeID,
			PublicKey: []byte{0x01},
			LastSeen:  lastSeen,
		}
	}

	mock.EXPECT().
		ListNodes(gomock.Any(), gomock.Any()).
		Return(rows, nil)

	store := &Store{q: mock}
	page, err := store.ListNodes(context.Background(), 0, []string{"YVR"}, nil, nil, nil, "", "", "", 0, 2, false)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(page.Items) != 2 {
		t.Errorf("expected 2 items, got %d", len(page.Items))
	}
	if !page.HasMore {
		t.Error("expected HasMore true")
	}
	if page.NextCursor == nil {
		t.Error("expected NextCursor to be set")
	}
}

func TestListNodes_IATAsUnmarshal(t *testing.T) {
	ctrl := gomock.NewController(t)
	mock := mockdb.NewMockQuerier(ctrl)

	nodeID := uuid.MustParse("00000000-0000-0000-0000-000000000001")
	iatasJSON := []byte(`[{"iata":"YVR","last_seen":1700000000000}]`)

	mock.EXPECT().
		ListNodes(gomock.Any(), gomock.Any()).
		Return([]sqlc.ListNodesRow{
			{
				ID:        nodeID,
				PublicKey: []byte{0x01},
				Iatas:     iatasJSON,
			},
		}, nil)

	store := &Store{q: mock}
	page, err := store.ListNodes(context.Background(), 0, nil, nil, nil, nil, "", "", "", 0, 10, false)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(page.Items[0].IATAs) != 1 {
		t.Errorf("expected 1 IATA, got %d", len(page.Items[0].IATAs))
	}
	if page.Items[0].IATAs[0].IATA != "YVR" {
		t.Errorf("expected IATA YVR, got %s", page.Items[0].IATAs[0].IATA)
	}
}

func TestListNodes_Stale(t *testing.T) {
	ctrl := gomock.NewController(t)
	mock := mockdb.NewMockQuerier(ctrl)

	staleID := uuid.MustParse("00000000-0000-0000-0000-000000000001")
	freshID := uuid.MustParse("00000000-0000-0000-0000-000000000002")
	unmeasuredID := uuid.MustParse("00000000-0000-0000-0000-000000000003")

	mock.EXPECT().
		ListNodes(gomock.Any(), gomock.Any()).
		Return([]sqlc.ListNodesRow{
			{ID: staleID, PublicKey: []byte{0x01}, LastSeen: pgtype.Timestamptz{Time: time.Now().Add(-48 * time.Hour), Valid: true}},
			{ID: freshID, PublicKey: []byte{0x02}, LastSeen: pgtype.Timestamptz{Time: time.Now(), Valid: true}},
			{ID: unmeasuredID, PublicKey: []byte{0x03}, LastSeen: pgtype.Timestamptz{Valid: false}},
		}, nil)

	store := &Store{q: mock, staleThreshold: 24 * time.Hour}
	page, err := store.ListNodes(context.Background(), 0, nil, nil, nil, nil, "", "", "", 0, 10, false)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	byID := make(map[uuid.UUID]bool)
	for _, n := range page.Items {
		byID[n.ID] = n.Stale
	}
	if !byID[staleID] {
		t.Error("expected node last seen 48h ago to be stale with a 24h threshold")
	}
	if byID[freshID] {
		t.Error("expected node last seen just now to not be stale")
	}
	if byID[unmeasuredID] {
		t.Error("expected a node with no last_seen at all to not be stale")
	}
}

func TestListNodes_RadioStringFormatting(t *testing.T) {
	ctrl := gomock.NewController(t)
	mock := mockdb.NewMockQuerier(ctrl)

	nodeID := uuid.MustParse("00000000-0000-0000-0000-000000000001")
	freq := float32(915.0)
	sf := int16(7)
	bw := float32(125.0)

	mock.EXPECT().
		ListNodes(gomock.Any(), gomock.Any()).
		Return([]sqlc.ListNodesRow{
			{
				ID:           nodeID,
				PublicKey:    []byte{0x01},
				RadioFreqMhz: &freq,
				RadioSf:      &sf,
				RadioBwKhz:   &bw,
			},
		}, nil)

	store := &Store{q: mock}
	page, err := store.ListNodes(context.Background(), 0, nil, nil, nil, nil, "", "", "", 0, 10, false)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if page.Items[0].Radio == nil {
		t.Fatal("expected Radio to be set")
	}
	if *page.Items[0].Radio != "915,125,7" {
		t.Errorf("expected Radio 915,125,7, got %s", *page.Items[0].Radio)
	}
}

func TestGetNode_LastAdvertAt(t *testing.T) {
	ctrl := gomock.NewController(t)
	mock := mockdb.NewMockQuerier(ctrl)

	nodeID := uuid.MustParse("00000000-0000-0000-0000-000000000001")
	lastAdvert := pgtype.Timestamptz{Time: time.UnixMilli(1700000000000), Valid: true}

	mock.EXPECT().
		GetNodeByID(gomock.Any(), nodeID).
		Return(sqlc.GetNodeByIDRow{
			ID:           nodeID,
			PublicKey:    []byte{0x01},
			FirstSeen:    pgtype.Timestamptz{Time: time.Now().Add(-time.Hour), Valid: true},
			LastSeen:     pgtype.Timestamptz{Time: time.Now(), Valid: true},
			LastAdvertAt: lastAdvert,
		}, nil)

	mock.EXPECT().
		GetNodeNeighbors(gomock.Any(), nodeID).
		Return([]sqlc.GetNodeNeighborsRow{}, nil)

	store := &Store{q: mock}
	node, err := store.GetNode(context.Background(), nodeID)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if node.LastAdvertAt == nil {
		t.Fatal("expected LastAdvertAt to be set")
	}
	if *node.LastAdvertAt != 1700000000000 {
		t.Errorf("expected LastAdvertAt 1700000000000, got %d", *node.LastAdvertAt)
	}
}

func TestGetNode_LastAdvertAtNil(t *testing.T) {
	ctrl := gomock.NewController(t)
	mock := mockdb.NewMockQuerier(ctrl)

	nodeID := uuid.MustParse("00000000-0000-0000-0000-000000000001")

	mock.EXPECT().
		GetNodeByID(gomock.Any(), nodeID).
		Return(sqlc.GetNodeByIDRow{
			ID:        nodeID,
			PublicKey: []byte{0x01},
			FirstSeen: pgtype.Timestamptz{Time: time.Now().Add(-time.Hour), Valid: true},
			LastSeen:  pgtype.Timestamptz{Time: time.Now(), Valid: true},
		}, nil)

	mock.EXPECT().
		GetNodeNeighbors(gomock.Any(), nodeID).
		Return([]sqlc.GetNodeNeighborsRow{}, nil)

	store := &Store{q: mock}
	node, err := store.GetNode(context.Background(), nodeID)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if node.LastAdvertAt != nil {
		t.Errorf("expected nil LastAdvertAt, got %d", *node.LastAdvertAt)
	}
}

func TestGetNode_Stale(t *testing.T) {
	ctrl := gomock.NewController(t)
	mock := mockdb.NewMockQuerier(ctrl)

	nodeID := uuid.MustParse("00000000-0000-0000-0000-000000000001")

	mock.EXPECT().
		GetNodeByID(gomock.Any(), nodeID).
		Return(sqlc.GetNodeByIDRow{
			ID:        nodeID,
			PublicKey: []byte{0x01},
			NodeType:  1, // companion -- Stale applies to every node type, unlike clock drift
			FirstSeen: pgtype.Timestamptz{Time: time.Now().Add(-72 * time.Hour), Valid: true},
			LastSeen:  pgtype.Timestamptz{Time: time.Now().Add(-48 * time.Hour), Valid: true},
		}, nil)

	mock.EXPECT().
		GetNodeNeighbors(gomock.Any(), nodeID).
		Return([]sqlc.GetNodeNeighborsRow{}, nil)

	store := &Store{q: mock, staleThreshold: 24 * time.Hour}
	node, err := store.GetNode(context.Background(), nodeID)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if !node.Stale {
		t.Error("expected node last seen 48h ago to be stale with a 24h threshold")
	}
}

func TestGetNode_ClockDrift_OutOfSync(t *testing.T) {
	ctrl := gomock.NewController(t)
	mock := mockdb.NewMockQuerier(ctrl)

	nodeID := uuid.MustParse("00000000-0000-0000-0000-000000000001")
	drift := int32(-600) // 10 minutes behind, beyond a 5m threshold

	mock.EXPECT().
		GetNodeByID(gomock.Any(), nodeID).
		Return(sqlc.GetNodeByIDRow{
			ID:                      nodeID,
			PublicKey:               []byte{0x01},
			NodeType:                2, // repeater
			FirstSeen:               pgtype.Timestamptz{Time: time.Now().Add(-time.Hour), Valid: true},
			LastSeen:                pgtype.Timestamptz{Time: time.Now(), Valid: true},
			LastAdvertAt:            pgtype.Timestamptz{Time: time.UnixMilli(1700000000000), Valid: true},
			DeviceClockDriftSeconds: &drift,
		}, nil)

	mock.EXPECT().
		GetNodeNeighbors(gomock.Any(), nodeID).
		Return([]sqlc.GetNodeNeighborsRow{}, nil)

	store := &Store{q: mock, clockDriftThreshold: 5 * time.Minute}
	node, err := store.GetNode(context.Background(), nodeID)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if node.ClockDriftSeconds == nil || *node.ClockDriftSeconds != -600 {
		t.Fatalf("expected ClockDriftSeconds -600, got %v", node.ClockDriftSeconds)
	}
	if node.ClockOutOfSync == nil || !*node.ClockOutOfSync {
		t.Errorf("expected ClockOutOfSync true, got %v", node.ClockOutOfSync)
	}
	if node.ClockCheckedAt == nil || *node.ClockCheckedAt != 1700000000000 {
		t.Errorf("expected ClockCheckedAt 1700000000000, got %v", node.ClockCheckedAt)
	}
}

func TestGetNode_ClockDrift_InSync(t *testing.T) {
	ctrl := gomock.NewController(t)
	mock := mockdb.NewMockQuerier(ctrl)

	nodeID := uuid.MustParse("00000000-0000-0000-0000-000000000001")
	drift := int32(30) // well within a 5m threshold

	mock.EXPECT().
		GetNodeByID(gomock.Any(), nodeID).
		Return(sqlc.GetNodeByIDRow{
			ID:                      nodeID,
			PublicKey:               []byte{0x01},
			NodeType:                3, // room server
			FirstSeen:               pgtype.Timestamptz{Time: time.Now().Add(-time.Hour), Valid: true},
			LastSeen:                pgtype.Timestamptz{Time: time.Now(), Valid: true},
			LastAdvertAt:            pgtype.Timestamptz{Time: time.UnixMilli(1700000000000), Valid: true},
			DeviceClockDriftSeconds: &drift,
		}, nil)

	mock.EXPECT().
		GetNodeNeighbors(gomock.Any(), nodeID).
		Return([]sqlc.GetNodeNeighborsRow{}, nil)

	store := &Store{q: mock, clockDriftThreshold: 5 * time.Minute}
	node, err := store.GetNode(context.Background(), nodeID)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if node.ClockOutOfSync == nil || *node.ClockOutOfSync {
		t.Errorf("expected ClockOutOfSync false, got %v", node.ClockOutOfSync)
	}
}

func TestGetNode_ClockDrift_OmittedForCompanion(t *testing.T) {
	ctrl := gomock.NewController(t)
	mock := mockdb.NewMockQuerier(ctrl)

	nodeID := uuid.MustParse("00000000-0000-0000-0000-000000000001")
	drift := int32(-600)

	mock.EXPECT().
		GetNodeByID(gomock.Any(), nodeID).
		Return(sqlc.GetNodeByIDRow{
			ID:                      nodeID,
			PublicKey:               []byte{0x01},
			NodeType:                1, // companion -- clock drift only applies to 2/3
			FirstSeen:               pgtype.Timestamptz{Time: time.Now().Add(-time.Hour), Valid: true},
			LastSeen:                pgtype.Timestamptz{Time: time.Now(), Valid: true},
			LastAdvertAt:            pgtype.Timestamptz{Time: time.UnixMilli(1700000000000), Valid: true},
			DeviceClockDriftSeconds: &drift,
		}, nil)

	mock.EXPECT().
		GetNodeNeighbors(gomock.Any(), nodeID).
		Return([]sqlc.GetNodeNeighborsRow{}, nil)

	store := &Store{q: mock, clockDriftThreshold: 5 * time.Minute}
	node, err := store.GetNode(context.Background(), nodeID)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if node.ClockDriftSeconds != nil || node.ClockOutOfSync != nil || node.ClockCheckedAt != nil {
		t.Error("expected all three clock fields nil for a companion node")
	}
}

func TestGetNode_ClockDrift_OmittedWhenUnmeasured(t *testing.T) {
	ctrl := gomock.NewController(t)
	mock := mockdb.NewMockQuerier(ctrl)

	nodeID := uuid.MustParse("00000000-0000-0000-0000-000000000001")

	mock.EXPECT().
		GetNodeByID(gomock.Any(), nodeID).
		Return(sqlc.GetNodeByIDRow{
			ID:        nodeID,
			PublicKey: []byte{0x01},
			NodeType:  2, // repeater, but no advert-derived drift yet
			FirstSeen: pgtype.Timestamptz{Time: time.Now().Add(-time.Hour), Valid: true},
			LastSeen:  pgtype.Timestamptz{Time: time.Now(), Valid: true},
		}, nil)

	mock.EXPECT().
		GetNodeNeighbors(gomock.Any(), nodeID).
		Return([]sqlc.GetNodeNeighborsRow{}, nil)

	store := &Store{q: mock, clockDriftThreshold: 5 * time.Minute}
	node, err := store.GetNode(context.Background(), nodeID)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if node.ClockDriftSeconds != nil || node.ClockOutOfSync != nil || node.ClockCheckedAt != nil {
		t.Error("expected all three clock fields nil when no advert has been measured yet")
	}
}

func TestGetNodesByIDs_Mapping(t *testing.T) {
	ctrl := gomock.NewController(t)
	mock := mockdb.NewMockQuerier(ctrl)

	nodeID := uuid.MustParse("00000000-0000-0000-0000-000000000001")
	name := "test-node"

	mock.EXPECT().
		GetNodesByIDs(gomock.Any(), []uuid.UUID{nodeID}).
		Return([]sqlc.GetNodesByIDsRow{
			{
				ID:        nodeID,
				Name:      &name,
				PublicKey: []byte{0xde, 0xad},
			},
		}, nil)

	store := &Store{q: mock}
	result, err := store.GetNodesByIDs(context.Background(), []uuid.UUID{nodeID})
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	node, ok := result[nodeID]
	if !ok {
		t.Fatal("expected nodeID in result map")
	}
	if node.PublicKey != "dead" {
		t.Errorf("expected PublicKey dead, got %s", node.PublicKey)
	}
}

func TestGetNodeNeighbors_Deduplication(t *testing.T) {
	ctrl := gomock.NewController(t)
	mock := mockdb.NewMockQuerier(ctrl)

	nodeID := uuid.MustParse("00000000-0000-0000-0000-000000000001")
	neighborID := uuid.MustParse("00000000-0000-0000-0000-000000000002")
	earlier := pgtype.Timestamptz{Time: time.UnixMilli(1700000000000), Valid: true}
	later := pgtype.Timestamptz{Time: time.UnixMilli(1700000001000), Valid: true}

	mock.EXPECT().
		GetNodeNeighbors(gomock.Any(), nodeID).
		Return([]sqlc.GetNodeNeighborsRow{
			{
				ID:               neighborID,
				PublicKey:        []byte{0x01},
				Iata:             "YVR",
				ObservationCount: 3,
				FirstSeen:        earlier,
				LastSeen:         earlier,
			},
			{
				ID:               neighborID,
				PublicKey:        []byte{0x01},
				Iata:             "YYJ",
				ObservationCount: 2,
				FirstSeen:        earlier,
				LastSeen:         later,
			},
		}, nil)

	store := &Store{q: mock}
	neighbors, err := store.GetNodeNeighbors(context.Background(), nodeID)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(neighbors) != 1 {
		t.Fatalf("expected 1 deduplicated neighbor, got %d", len(neighbors))
	}
	if neighbors[0].ObservationCount != 5 {
		t.Errorf("expected ObservationCount 5, got %d", neighbors[0].ObservationCount)
	}
	if neighbors[0].IATA != "YYJ" {
		t.Errorf("expected IATA YYJ (most recent), got %s", neighbors[0].IATA)
	}
}

func TestGetNodeNeighbors_DBError(t *testing.T) {
	ctrl := gomock.NewController(t)
	mock := mockdb.NewMockQuerier(ctrl)

	nodeID := uuid.MustParse("00000000-0000-0000-0000-000000000001")

	mock.EXPECT().
		GetNodeNeighbors(gomock.Any(), nodeID).
		Return(nil, errors.New("db error"))

	store := &Store{q: mock}
	_, err := store.GetNodeNeighbors(context.Background(), nodeID)
	if err == nil {
		t.Fatal("expected error, got nil")
	}
}

func TestListNodes_IncludeNeighbors_PassesFlagAndMapsIDs(t *testing.T) {
	ctrl := gomock.NewController(t)
	mock := mockdb.NewMockQuerier(ctrl)

	nodeID := uuid.MustParse("00000000-0000-0000-0000-000000000001")
	neighborID := uuid.MustParse("00000000-0000-0000-0000-000000000002")

	mock.EXPECT().
		ListNodes(gomock.Any(), gomock.Eq(sqlc.ListNodesParams{
			Column1: int16(0), Column2: nil, Column3: "any", Column4: "any",
			Column5: nil, Column6: "", Column7: pgtype.Timestamptz{},
			Limit: 11, Column9: "", Column10: true,
		})).
		Return([]sqlc.ListNodesRow{
			{
				ID:          nodeID,
				PublicKey:   []byte{0x01},
				NeighborIds: []uuid.UUID{neighborID},
			},
		}, nil)

	store := &Store{q: mock}
	page, err := store.ListNodes(context.Background(), 0, nil, nil, nil, nil, "", "", "", 0, 10, true)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(page.Items[0].NeighborIDs) != 1 || page.Items[0].NeighborIDs[0] != neighborID {
		t.Errorf("expected NeighborIDs [%s], got %v", neighborID, page.Items[0].NeighborIDs)
	}
}

func TestListNodes_ExcludeNeighbors_LeavesIDsNil(t *testing.T) {
	ctrl := gomock.NewController(t)
	mock := mockdb.NewMockQuerier(ctrl)

	nodeID := uuid.MustParse("00000000-0000-0000-0000-000000000001")

	mock.EXPECT().
		ListNodes(gomock.Any(), gomock.Eq(sqlc.ListNodesParams{
			Column1: int16(0), Column2: nil, Column3: "any", Column4: "any",
			Column5: nil, Column6: "", Column7: pgtype.Timestamptz{},
			Limit: 11, Column9: "", Column10: false,
		})).
		Return([]sqlc.ListNodesRow{
			{ID: nodeID, PublicKey: []byte{0x01}, NeighborIds: nil},
		}, nil)

	store := &Store{q: mock}
	page, err := store.ListNodes(context.Background(), 0, nil, nil, nil, nil, "", "", "", 0, 10, false)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if page.Items[0].NeighborIDs != nil {
		t.Errorf("expected NeighborIDs to stay nil when includeNeighbors is false, got %v", page.Items[0].NeighborIDs)
	}
}

func TestDeleteOldNodes(t *testing.T) {
	ctrl := gomock.NewController(t)
	mock := mockdb.NewMockQuerier(ctrl)

	cutoff := time.Now().Add(-30 * 24 * time.Hour)

	mock.EXPECT().
		DeleteOldNodes(gomock.Any(), gomock.Eq(pgtype.Timestamptz{Time: cutoff, Valid: true})).
		Return(nil)

	store := &Store{q: mock}
	if err := store.DeleteOldNodes(context.Background(), cutoff); err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
}
