import 'dart:async';

/// Request to schedule one notification through a platform adapter.
sealed class NotificationRequest {
  /// Creates a notification request.
  factory NotificationRequest({
    required String id,
    required String title,
    required String body,
    required DateTime scheduledDateTimeUtc,
    String payloadJson = '{}',
  }) {
    return _NotificationRequest(
      id: _requiredTrimmed(id, 'id'),
      title: _requiredTrimmed(title, 'title'),
      body: body,
      scheduledDateTimeUtc: scheduledDateTimeUtc.toUtc(),
      payloadJson: payloadJson,
    );
  }

  const NotificationRequest._({
    required this.id,
    required this.title,
    required this.body,
    required this.scheduledDateTimeUtc,
    required this.payloadJson,
  });

  /// Stable notification id used for scheduling and cancellation.
  final String id;

  /// User-visible notification title.
  final String title;

  /// User-visible notification body.
  final String body;

  /// UTC instant when the notification should be delivered.
  final DateTime scheduledDateTimeUtc;

  /// Adapter-neutral JSON payload string.
  final String payloadJson;
}

final class _NotificationRequest extends NotificationRequest {
  const _NotificationRequest({
    required super.id,
    required super.title,
    required super.body,
    required super.scheduledDateTimeUtc,
    required super.payloadJson,
  }) : super._();
}

/// User feedback category emitted by a notification adapter.
enum NotificationFeedbackType {
  /// The notification was opened or clicked.
  clicked,

  /// The notification was dismissed.
  dismissed,

  /// A notification action was selected.
  action,
}

/// Feedback event emitted by a notification adapter.
final class NotificationFeedback {
  /// Creates a notification feedback event.
  NotificationFeedback({
    required String id,
    required this.type,
    this.actionId,
    this.payloadJson = '{}',
  }) : id = _requiredTrimmed(id, 'id');

  /// Stable notification id associated with the feedback.
  final String id;

  /// Type of user feedback.
  final NotificationFeedbackType type;

  /// Optional action id for [NotificationFeedbackType.action].
  final String? actionId;

  /// Adapter-neutral JSON payload string.
  final String payloadJson;
}

/// Platform-neutral notification adapter boundary.
abstract interface class NotificationAdapter {
  /// Schedule [request] for future delivery.
  Future<void> schedule(NotificationRequest request);

  /// Cancel a pending notification by stable [id].
  Future<void> cancel(String id);

  /// Stream of user feedback events from delivered notifications.
  Stream<NotificationFeedback> get feedback;
}

/// Test fake for notification adapter behavior.
final class FakeNotificationAdapter implements NotificationAdapter {
  final StreamController<NotificationFeedback> _feedbackController =
      StreamController<NotificationFeedback>.broadcast();
  final Map<String, NotificationRequest> _scheduled =
      <String, NotificationRequest>{};
  final List<String> _cancelledIds = <String>[];

  /// Snapshot of currently scheduled requests by id.
  Map<String, NotificationRequest> get scheduledRequests =>
      Map<String, NotificationRequest>.unmodifiable(_scheduled);

  /// Snapshot of cancellation ids in call order.
  List<String> get cancelledIds => List<String>.unmodifiable(_cancelledIds);

  @override
  Stream<NotificationFeedback> get feedback => _feedbackController.stream;

  @override
  Future<void> schedule(NotificationRequest request) async {
    _scheduled[request.id] = request;
  }

  @override
  Future<void> cancel(String id) async {
    final trimmed = _requiredTrimmed(id, 'id');
    _scheduled.remove(trimmed);
    _cancelledIds.add(trimmed);
  }

  /// Emit [event] to feedback listeners.
  void emitFeedback(NotificationFeedback event) {
    _feedbackController.add(event);
  }

  /// Release stream resources held by this fake.
  Future<void> close() {
    return _feedbackController.close();
  }
}

String _requiredTrimmed(String value, String name) {
  final trimmed = value.trim();
  if (trimmed.isEmpty) {
    throw ArgumentError.value(value, name, 'Value must not be blank.');
  }
  return trimmed;
}
