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

package handlers

import (
	"context"
	"encoding/json"
	"time"

	"github.com/MeshCore-Beacon/beacon-server/internal/api"
	"github.com/google/uuid"
)

// stubReader satisfies api.Reader with configurable function fields.
// Unset fields return zero values. Use it for both validation tests
// (leave all fields nil) and happy path tests (set only what you need).
type stubReader struct {
	listIATAs                    func(ctx context.Context) ([]api.IATA, error)
	getIATA                      func(ctx context.Context, iata string) (*api.IATA, error)
	getIATABorder                func(ctx context.Context, iata string) (json.RawMessage, error)
	listRegions                  func(ctx context.Context) ([]api.RegionSummary, error)
	getRegion                    func(ctx context.Context, regionID int32) (*api.Region, error)
	getRegionBySlug              func(ctx context.Context, slug string) (*api.Region, error)
	listChannels                 func(ctx context.Context, limit int32, hash []byte, iatas []string, cursor int64) (api.Page[api.ChannelSummary], error)
	getChannel                   func(ctx context.Context, channelID int32) (*api.Channel, error)
	listChannelMessages          func(ctx context.Context, channelID *int32, since time.Time, limit int32, iatas []string, scope string, cursor int64) (api.Page[api.ChannelMessage], error)
	listChannelMessagesByHash    func(ctx context.Context, hash []byte, since time.Time, limit int32, iatas []string, scope string, cursor int64) (api.Page[api.ChannelMessage], error)
	listMessagesAfterID          func(ctx context.Context, afterID int64, iatas []string, scope string, limit int32) ([]api.ChannelMessage, error)
	listObservers                func(ctx context.Context, iatas []string, observerType, broker, status, name, scope string, cursor int64, limit int32) (api.Page[api.ObserverSummary], error)
	getObserver                  func(ctx context.Context, observerID uuid.UUID) (*api.Observer, error)
	getObserverTelemetry         func(ctx context.Context, observerID uuid.UUID, since, until time.Time, afterID int64) (*api.ObserverTelemetry, error)
	getObserverTelemetryBucketed func(ctx context.Context, observerID uuid.UUID, since, until time.Time, bucketHours int32) ([]api.ObserverTelemetryPoint, error)
	getObserverScopes            func(ctx context.Context, observerID uuid.UUID) ([]string, error)
	listObserverAdverts          func(ctx context.Context, observerID uuid.UUID, cursor int64, limit int32) (api.Page[api.AdvertObservation], error)
	listNodes                    func(ctx context.Context, nodeType int16, iatas []string, supportsMultibytePaths, supportsMultibyteTraces *bool, pubkey []byte, pubkeyPrefix, name, scope string, cursor int64, limit int32, includeNeighbors bool) (api.Page[api.NodeSummary], error)
	getNode                      func(ctx context.Context, nodeID uuid.UUID) (*api.Node, error)
	getNodeNeighbors             func(ctx context.Context, nodeID uuid.UUID) ([]api.NodeNeighbor, error)
	listNodeObservations         func(ctx context.Context, nodeID uuid.UUID, cursor int64, limit int32) (api.Page[api.PacketObservationSummary], error)
	listPackets                  func(ctx context.Context, payloadTypes, routeTypes []int16, iatas []string, scopes []string, since, until time.Time, cursor int64, limit int32) (api.Page[api.PacketSummary], error)
	listPacketsAfterID           func(ctx context.Context, afterObservationID int64, payloadType, routeType int16, iatas []string, scope string, limit int32) ([]api.PacketSummary, error)
	getPacket                    func(ctx context.Context, packetHash []byte) (*api.Packet, error)
	getRadioPresets              func(ctx context.Context, preset string, iatas []string) ([]api.RadioPreset, error)
	getStatsOverview             func(ctx context.Context, iatas []string) (*api.StatsOverview, error)
	getStatsObservations         func(ctx context.Context, iatas []string, since time.Time) ([]api.ObservationPoint, error)
	getStatsPayloadBreakdown     func(ctx context.Context, iatas []string, since time.Time) ([]api.PayloadBreakdownItem, error)
	getStatsTopNodes             func(ctx context.Context, iatas []string, limit int32) ([]api.TopNode, error)
	getStatsTopObservers         func(ctx context.Context, iatas []string, since time.Time, limit int32) ([]api.TopObserver, error)
	getStatsTopAdvertisers       func(ctx context.Context, iatas []string, since time.Time, limit int32) ([]api.TopAdvertiser, error)
	getStatsClockDrift           func(ctx context.Context, iatas []string, limit int32) ([]api.ClockDriftEntry, error)
	getStatsTopTalkers           func(ctx context.Context, iatas []string, since time.Time, limit int32) ([]api.TopTalker, error)
	getScopeStats                func(ctx context.Context) ([]api.ScopeStats, error)
	getStatsNodeTypes            func(ctx context.Context, iatas []string) ([]api.NodeTypeCount, error)
	getScopeNames                func(ctx context.Context) ([]string, error)
	getScopesByIATAs             func(ctx context.Context, iatas []string) ([]api.ScopeSummary, error)
	getScopeByName               func(ctx context.Context, name string) (*api.ScopeDetail, error)
	listTraceTags                func(ctx context.Context, iatas []string, scope, traceType string, since, until time.Time, cursor time.Time, limit int32) ([]api.TraceTagSummary, error)
	getTraceByTag                func(ctx context.Context, tag string) (*api.TraceDetail, error)
	listKnownRoutes              func(ctx context.Context, iata string, hopCount int32, cursor time.Time, limit int32) ([]api.KnownRoute, error)
	searchKnownRoutes            func(ctx context.Context, iata, fromHash, toHash string) ([]api.KnownRoute, error)
	getKnownRoutesByNode         func(ctx context.Context, iata string, nodeID uuid.UUID) ([]api.KnownRoute, error)
	getCrossIATANeighbors        func(ctx context.Context, nodeID uuid.UUID, iata string) ([]api.NodeNeighbor, error)
	searchCrossIATARoutes        func(ctx context.Context, fromHash, fromIATA, toHash, toIATA string) ([]api.CrossIATARoute, error)
	getNodesByIDs                func(ctx context.Context, ids []uuid.UUID) (map[uuid.UUID]*api.ResolvedNode, error)
}

func (s stubReader) ListIATAs(ctx context.Context) ([]api.IATA, error) {
	if s.listIATAs != nil {
		return s.listIATAs(ctx)
	}
	return nil, nil
}

func (s stubReader) GetIATA(ctx context.Context, iata string) (*api.IATA, error) {
	if s.getIATA != nil {
		return s.getIATA(ctx, iata)
	}
	return nil, nil
}

func (s stubReader) GetIATABorder(ctx context.Context, iata string) (json.RawMessage, error) {
	if s.getIATABorder != nil {
		return s.getIATABorder(ctx, iata)
	}
	return nil, nil
}

func (s stubReader) ListRegions(ctx context.Context) ([]api.RegionSummary, error) {
	if s.listRegions != nil {
		return s.listRegions(ctx)
	}
	return nil, nil
}

func (s stubReader) GetRegion(ctx context.Context, regionID int32) (*api.Region, error) {
	if s.getRegion != nil {
		return s.getRegion(ctx, regionID)
	}
	return nil, nil
}

func (s stubReader) GetRegionBySlug(ctx context.Context, slug string) (*api.Region, error) {
	if s.getRegionBySlug != nil {
		return s.getRegionBySlug(ctx, slug)
	}
	return nil, nil
}

func (s stubReader) ListChannels(ctx context.Context, limit int32, hash []byte, iatas []string, cursor int64) (api.Page[api.ChannelSummary], error) {
	if s.listChannels != nil {
		return s.listChannels(ctx, limit, hash, iatas, cursor)
	}
	return api.Page[api.ChannelSummary]{}, nil
}

func (s stubReader) GetChannel(ctx context.Context, channelID int32) (*api.Channel, error) {
	if s.getChannel != nil {
		return s.getChannel(ctx, channelID)
	}
	return nil, nil
}

func (s stubReader) ListChannelMessages(ctx context.Context, channelID *int32, since time.Time, limit int32, iatas []string, scope string, cursor int64) (api.Page[api.ChannelMessage], error) {
	if s.listChannelMessages != nil {
		return s.listChannelMessages(ctx, channelID, since, limit, iatas, scope, cursor)
	}
	return api.Page[api.ChannelMessage]{}, nil
}

func (s stubReader) ListChannelMessagesByHash(ctx context.Context, hash []byte, since time.Time, limit int32, iatas []string, scope string, cursor int64) (api.Page[api.ChannelMessage], error) {
	if s.listChannelMessagesByHash != nil {
		return s.listChannelMessagesByHash(ctx, hash, since, limit, iatas, scope, cursor)
	}
	return api.Page[api.ChannelMessage]{}, nil
}

func (s stubReader) ListMessagesAfterID(ctx context.Context, afterID int64, iatas []string, scope string, limit int32) ([]api.ChannelMessage, error) {
	if s.listMessagesAfterID != nil {
		return s.listMessagesAfterID(ctx, afterID, iatas, scope, limit)
	}
	return nil, nil
}

func (s stubReader) ListObservers(ctx context.Context, iatas []string, observerType, broker, status, name, scope string, cursor int64, limit int32) (api.Page[api.ObserverSummary], error) {
	if s.listObservers != nil {
		return s.listObservers(ctx, iatas, observerType, broker, status, name, scope, cursor, limit)
	}
	return api.Page[api.ObserverSummary]{}, nil
}

func (s stubReader) GetObserver(ctx context.Context, observerID uuid.UUID) (*api.Observer, error) {
	if s.getObserver != nil {
		return s.getObserver(ctx, observerID)
	}
	return nil, nil
}

func (s stubReader) GetObserverTelemetry(ctx context.Context, observerID uuid.UUID, since, until time.Time, afterID int64) (*api.ObserverTelemetry, error) {
	if s.getObserverTelemetry != nil {
		return s.getObserverTelemetry(ctx, observerID, since, until, afterID)
	}
	return nil, nil
}

func (s stubReader) GetObserverTelemetryBucketed(ctx context.Context, observerID uuid.UUID, since, until time.Time, bucketHours int32) ([]api.ObserverTelemetryPoint, error) {
	if s.getObserverTelemetryBucketed != nil {
		return s.getObserverTelemetryBucketed(ctx, observerID, since, until, bucketHours)
	}
	return nil, nil
}

func (s stubReader) GetObserverScopes(ctx context.Context, observerID uuid.UUID) ([]string, error) {
	if s.getObserverScopes != nil {
		return s.getObserverScopes(ctx, observerID)
	}
	return nil, nil
}

func (s stubReader) ListObserverAdverts(ctx context.Context, observerID uuid.UUID, cursor int64, limit int32) (api.Page[api.AdvertObservation], error) {
	if s.listObserverAdverts != nil {
		return s.listObserverAdverts(ctx, observerID, cursor, limit)
	}
	return api.Page[api.AdvertObservation]{}, nil
}

func (s stubReader) ListNodes(ctx context.Context, nodeType int16, iatas []string, supportsMultibytePaths, supportsMultibyteTraces *bool, pubkey []byte, pubkeyPrefix, name, scope string, cursor int64, limit int32, includeNeighbors bool) (api.Page[api.NodeSummary], error) {
	if s.listNodes != nil {
		return s.listNodes(ctx, nodeType, iatas, supportsMultibytePaths, supportsMultibyteTraces, pubkey, pubkeyPrefix, name, scope, cursor, limit, includeNeighbors)
	}
	return api.Page[api.NodeSummary]{}, nil
}

func (s stubReader) GetNode(ctx context.Context, nodeID uuid.UUID) (*api.Node, error) {
	if s.getNode != nil {
		return s.getNode(ctx, nodeID)
	}
	return nil, nil
}

func (s stubReader) GetNodeNeighbors(ctx context.Context, nodeID uuid.UUID) ([]api.NodeNeighbor, error) {
	if s.getNodeNeighbors != nil {
		return s.getNodeNeighbors(ctx, nodeID)
	}
	return nil, nil
}

func (s stubReader) ListNodeObservations(ctx context.Context, nodeID uuid.UUID, cursor int64, limit int32) (api.Page[api.PacketObservationSummary], error) {
	if s.listNodeObservations != nil {
		return s.listNodeObservations(ctx, nodeID, cursor, limit)
	}
	return api.Page[api.PacketObservationSummary]{}, nil
}

func (s stubReader) ListPackets(ctx context.Context, payloadTypes, routeTypes []int16, iatas []string, scopes []string, since, until time.Time, cursor int64, limit int32) (api.Page[api.PacketSummary], error) {
	if s.listPackets != nil {
		return s.listPackets(ctx, payloadTypes, routeTypes, iatas, scopes, since, until, cursor, limit)
	}
	return api.Page[api.PacketSummary]{}, nil
}

func (s stubReader) ListPacketsAfterID(ctx context.Context, afterObservationID int64, payloadType, routeType int16, iatas []string, scope string, limit int32) ([]api.PacketSummary, error) {
	if s.listPacketsAfterID != nil {
		return s.listPacketsAfterID(ctx, afterObservationID, payloadType, routeType, iatas, scope, limit)
	}
	return nil, nil
}

func (s stubReader) GetPacket(ctx context.Context, packetHash []byte) (*api.Packet, error) {
	if s.getPacket != nil {
		return s.getPacket(ctx, packetHash)
	}
	return nil, nil
}

func (s stubReader) GetRadioPresets(ctx context.Context, preset string, iatas []string) ([]api.RadioPreset, error) {
	if s.getRadioPresets != nil {
		return s.getRadioPresets(ctx, preset, iatas)
	}
	return nil, nil
}

func (s stubReader) GetStatsOverview(ctx context.Context, iatas []string) (*api.StatsOverview, error) {
	if s.getStatsOverview != nil {
		return s.getStatsOverview(ctx, iatas)
	}
	return nil, nil
}

func (s stubReader) GetStatsObservations(ctx context.Context, iatas []string, since time.Time) ([]api.ObservationPoint, error) {
	if s.getStatsObservations != nil {
		return s.getStatsObservations(ctx, iatas, since)
	}
	return nil, nil
}

func (s stubReader) GetStatsPayloadBreakdown(ctx context.Context, iatas []string, since time.Time) ([]api.PayloadBreakdownItem, error) {
	if s.getStatsPayloadBreakdown != nil {
		return s.getStatsPayloadBreakdown(ctx, iatas, since)
	}
	return nil, nil
}

func (s stubReader) GetStatsTopNodes(ctx context.Context, iatas []string, limit int32) ([]api.TopNode, error) {
	if s.getStatsTopNodes != nil {
		return s.getStatsTopNodes(ctx, iatas, limit)
	}
	return nil, nil
}

func (s stubReader) GetStatsTopObservers(ctx context.Context, iatas []string, since time.Time, limit int32) ([]api.TopObserver, error) {
	if s.getStatsTopObservers != nil {
		return s.getStatsTopObservers(ctx, iatas, since, limit)
	}
	return nil, nil
}

func (s stubReader) GetStatsTopAdvertisers(ctx context.Context, iatas []string, since time.Time, limit int32) ([]api.TopAdvertiser, error) {
	if s.getStatsTopAdvertisers != nil {
		return s.getStatsTopAdvertisers(ctx, iatas, since, limit)
	}
	return nil, nil
}

func (s stubReader) GetStatsClockDrift(ctx context.Context, iatas []string, limit int32) ([]api.ClockDriftEntry, error) {
	if s.getStatsClockDrift != nil {
		return s.getStatsClockDrift(ctx, iatas, limit)
	}
	return nil, nil
}

func (s stubReader) GetStatsTopTalkers(ctx context.Context, iatas []string, since time.Time, limit int32) ([]api.TopTalker, error) {
	if s.getStatsTopTalkers != nil {
		return s.getStatsTopTalkers(ctx, iatas, since, limit)
	}
	return nil, nil
}

func (s stubReader) GetScopeStats(ctx context.Context) ([]api.ScopeStats, error) {
	if s.getScopeStats != nil {
		return s.getScopeStats(ctx)
	}
	return nil, nil
}

func (s stubReader) GetStatsNodeTypes(ctx context.Context, iatas []string) ([]api.NodeTypeCount, error) {
	if s.getStatsNodeTypes != nil {
		return s.getStatsNodeTypes(ctx, iatas)
	}
	return nil, nil
}

func (s stubReader) GetScopeNames(ctx context.Context) ([]string, error) {
	if s.getScopeNames != nil {
		return s.getScopeNames(ctx)
	}
	return nil, nil
}

func (s stubReader) GetScopesByIATAs(ctx context.Context, iatas []string) ([]api.ScopeSummary, error) {
	if s.getScopesByIATAs != nil {
		return s.getScopesByIATAs(ctx, iatas)
	}
	return nil, nil
}

func (s stubReader) GetScopeByName(ctx context.Context, name string) (*api.ScopeDetail, error) {
	if s.getScopeByName != nil {
		return s.getScopeByName(ctx, name)
	}
	return nil, nil
}

func (s stubReader) ListTraceTags(ctx context.Context, iatas []string, scope, traceType string, since, until time.Time, cursor time.Time, limit int32) ([]api.TraceTagSummary, error) {
	if s.listTraceTags != nil {
		return s.listTraceTags(ctx, iatas, scope, traceType, since, until, cursor, limit)
	}
	return nil, nil
}

func (s stubReader) GetTraceByTag(ctx context.Context, tag string) (*api.TraceDetail, error) {
	if s.getTraceByTag != nil {
		return s.getTraceByTag(ctx, tag)
	}
	return nil, nil
}

func (s stubReader) ListKnownRoutes(ctx context.Context, iata string, hopCount int32, cursor time.Time, limit int32) ([]api.KnownRoute, error) {
	if s.listKnownRoutes != nil {
		return s.listKnownRoutes(ctx, iata, hopCount, cursor, limit)
	}
	return nil, nil
}

func (s stubReader) SearchKnownRoutes(ctx context.Context, iata, fromHash, toHash string) ([]api.KnownRoute, error) {
	if s.searchKnownRoutes != nil {
		return s.searchKnownRoutes(ctx, iata, fromHash, toHash)
	}
	return nil, nil
}

func (s stubReader) GetKnownRoutesByNode(ctx context.Context, iata string, nodeID uuid.UUID) ([]api.KnownRoute, error) {
	if s.getKnownRoutesByNode != nil {
		return s.getKnownRoutesByNode(ctx, iata, nodeID)
	}
	return nil, nil
}

func (s stubReader) GetCrossIATANeighbors(ctx context.Context, nodeID uuid.UUID, iata string) ([]api.NodeNeighbor, error) {
	if s.getCrossIATANeighbors != nil {
		return s.getCrossIATANeighbors(ctx, nodeID, iata)
	}
	return nil, nil
}

func (s stubReader) SearchCrossIATARoutes(ctx context.Context, fromHash, fromIATA, toHash, toIATA string) ([]api.CrossIATARoute, error) {
	if s.searchCrossIATARoutes != nil {
		return s.searchCrossIATARoutes(ctx, fromHash, fromIATA, toHash, toIATA)
	}
	return nil, nil
}

func (s stubReader) GetNodesByIDs(ctx context.Context, ids []uuid.UUID) (map[uuid.UUID]*api.ResolvedNode, error) {
	if s.getNodesByIDs != nil {
		return s.getNodesByIDs(ctx, ids)
	}
	return nil, nil
}
