// SPDX-FileCopyrightText: 2026 FocusFlow contributors
// SPDX-License-Identifier: AGPL-3.0-only

/// Tests Desktop Notification Adapter behavior for the Scheduler Notifications Desktop package.
library;

import 'dart:io';

import 'package:scheduler_notifications/notifications.dart';
import 'package:test/test.dart';

import '../../scheduler_notifications/test/notification_adapter_contract.dart';

// ignore: avoid_relative_lib_imports
import '../lib/desktop_notifications.dart';

/// Entry point for this executable Dart file.
/// It wires the local command, test, or application behavior needed by this repository without exposing additional public API.
void main() {
  runNotificationAdapterContractTests('desktop', () {
    final backend = _RecordingBackend();
    return NotificationAdapterContractHarness(
      adapter: DesktopNotificationAdapter(backend: backend),
      scheduledRequests: () => backend.delivered,
      cancelledIds: () => backend.cancelled,
    );
  });

  test('desktop adapter delegates schedule and cancel to backend', () async {
    final backend = _RecordingBackend();
    final adapter = DesktopNotificationAdapter(backend: backend);
    final request = _request('reminder-1');

    await adapter.schedule(request);
    await adapter.cancel(' reminder-1 ');

    expect(backend.delivered, [request]);
    expect(backend.cancelled, ['reminder-1']);
    expect(adapter, isA<NotificationAdapter>());
  });

  test('linux backend invokes notify-send', () async {
    final calls = <_ProcessCall>[];
    final logs = <String>[];
    final backend = createDefaultDesktopNotificationBackend(
      platform: DesktopNotificationPlatform.linux,
      processRunner: (String executable, List<String> arguments) async {
        calls.add(_ProcessCall(executable, arguments));
        return ProcessResult(1, 0, '', '');
      },
      logSink: logs.add,
    );

    await backend.deliver(_request('reminder-1'));

    expect(backend.isSupported, isTrue);
    expect(calls.single.executable, 'notify-send');
    expect(calls.single.arguments, contains('Start task'));
    expect(logs, isEmpty);
  });

  test('macOS backend invokes osascript', () async {
    final calls = <_ProcessCall>[];
    final backend = createDefaultDesktopNotificationBackend(
      platform: DesktopNotificationPlatform.macos,
      processRunner: (String executable, List<String> arguments) async {
        calls.add(_ProcessCall(executable, arguments));
        return ProcessResult(1, 0, '', '');
      },
    );

    await backend.deliver(_request('reminder-1'));

    expect(backend.isSupported, isTrue);
    expect(calls.single.executable, 'osascript');
    expect(calls.single.arguments, contains('-e'));
  });

  test('unsupported and Windows platforms fall back to log output', () async {
    final logs = <String>[];
    final windows = createDefaultDesktopNotificationBackend(
      platform: DesktopNotificationPlatform.windows,
      logSink: logs.add,
    );
    final unsupported = createDefaultDesktopNotificationBackend(
      platform: DesktopNotificationPlatform.unsupported,
      logSink: logs.add,
    );

    await windows.deliver(_request('windows-reminder'));
    await unsupported.cancel(' unsupported-reminder ');

    expect(windows.isSupported, isFalse);
    expect(unsupported.isSupported, isFalse);
    expect(logs.first, contains('windows-reminder'));
    expect(logs.last, contains('unsupported-reminder'));
  });

  test('fake adapter can stand in for scheduling workflows', () async {
    final adapter = FakeNotificationAdapter();
    addTearDown(adapter.close);
    await _scheduleAndCancelReminder(adapter, _request('reminder-1'));

    expect(adapter.scheduledRequests, isEmpty);
    expect(adapter.cancelledIds, ['reminder-1']);
  });

  test('default desktop adapter can be created on supported test platforms',
      () {
    if (!Platform.isLinux && !Platform.isMacOS && !Platform.isWindows) {
      markTestSkipped(
          'Desktop notifications are unsupported on this platform.');
    }

    expect(DesktopNotificationAdapter.defaultInstance(),
        isA<NotificationAdapter>());
  });
}

/// Top-level helper that performs the `_scheduleAndCancelReminder` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
Future<void> _scheduleAndCancelReminder(
  NotificationAdapter adapter,
  NotificationRequest request,
) async {
  await adapter.schedule(request);
  await adapter.cancel(request.id);
}

/// Top-level helper that performs the `_request` operation for this file.
/// Keeping the helper private makes the surrounding flow easier to read while preventing unrelated libraries from depending on implementation detail.
NotificationRequest _request(String id) {
  return NotificationRequest(
    id: id,
    title: 'Start task',
    body: 'Time to begin.',
    scheduledDateTimeUtc: DateTime.utc(2026, 1, 1, 9),
  );
}

/// Private implementation type for `_RecordingBackend` in this library.
/// It keeps supporting state and behavior scoped to the owning file so the public API stays focused on the scheduler concepts that callers use directly.
final class _RecordingBackend implements DesktopNotificationBackend {
  /// Stores the `delivered` value for this object.
  /// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
  final List<NotificationRequest> delivered = <NotificationRequest>[];

  /// Stores the `cancelled` value for this object.
  /// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
  final List<String> cancelled = <String>[];

  /// Returns the derived `isSupported` value for this object.
  /// Keeping this as an accessor gives callers a stable read surface while the owning type controls how the value is calculated.
  @override
  bool get isSupported => true;

  /// Runs the `deliver` operation and completes after its asynchronous work finishes.
  /// Callers should await the returned future so repository writes, UI refreshes, and failure handling happen in a predictable order.
  @override
  Future<void> deliver(NotificationRequest request) async {
    delivered.add(request);
  }

  /// Runs the `cancel` operation and completes after its asynchronous work finishes.
  /// Callers should await the returned future so repository writes, UI refreshes, and failure handling happen in a predictable order.
  @override
  Future<void> cancel(String id) async {
    cancelled.add(id);
  }
}

/// Private implementation type for `_ProcessCall` in this library.
/// It keeps supporting state and behavior scoped to the owning file so the public API stays focused on the scheduler concepts that callers use directly.
final class _ProcessCall {
  /// Creates a `_ProcessCall` instance with the values required by its invariants.
  /// Callers should pass already-normalized scheduler data because validation and defaulting belong at the model or service boundary, not in arbitrary UI code.
  const _ProcessCall(this.executable, this.arguments);

  /// Stores the `executable` value for this object.
  /// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
  final String executable;

  /// Stores the `arguments` value for this object.
  /// This field is part of the explicit state that downstream scheduling, persistence, or UI code reads when composing behavior.
  final List<String> arguments;
}
