// SWE.4 unit tests for MacOSPermissionsService — the Dart-side
// integration layer for the Swift `MacOSPermissionsHandler`
// (SRS-198, SRS-297, SRS-300, SysRS-166, SDD-091).
//
// Requirement trace:
//   Verification-plan row: SWE4-UV-XXX.
//   SRS-198 (Push-to-talk system permission acquisition).
//   SRS-297 / SRS-300 (Input Monitoring for global PTT on macOS).
//   SysRS-166 (Desktop notifications).
//   SDD-091 (PTT capability badge — live capability level).
//
// Strategy: Following the pattern in
// `android_permissions_service_test.dart`, use
// `TestDefaultBinaryMessengerBinding` to (a) capture outbound
// method invocations and (b) inject inbound state-change calls as
// if Swift had emitted them.
//
// Platform note: these tests run on macOS. The static `_isMacOS`
// check inside the service evaluates to true at field-initialization
// time, so the ValueNotifiers seed to `unknown` (not `granted`).
// This is intentional — on macOS the service must query the native
// side before it knows the real state. Tests that inject a channel
// observe `unknown` as the initial value and drive transitions from
// there. The channel-null short-circuit test documents that when
// channel is null, outbound calls are suppressed and each method
// returns its safe default.

import 'dart:io' show Platform;

import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';

import 'package:chanora_flutter/services/macos_permissions_service.dart';

void main() {
  TestWidgetsFlutterBinding.ensureInitialized();

  /// Whether the test host is actually macOS — the field initializers
  /// inside `MacOSPermissionsService` use `Platform.isMacOS` (not
  /// injectable), so the initial state values depend on this.
  final bool hostIsMacOS = !kIsWeb && Platform.isMacOS;

  late MethodChannel channel;
  late List<MethodCall> outgoingCalls;

  /// Intercept outbound calls AFTER the service's handler is installed.
  /// We store a reference so outbound invokeMethod calls can be
  /// captured while inbound handlePlatformMessage calls are routed
  /// to the service's handler.
  Future<Object?> Function(MethodCall call)? outgoingResponder;

  Future<void> sendInputMonitoringStateChanged({
    required String state,
  }) async {
    const codec = StandardMethodCodec();
    final encoded = codec.encodeMethodCall(
      MethodCall(methodInputMonitoringStateChanged, <String, dynamic>{
        'state': state,
      }),
    );
    await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
        .handlePlatformMessage(channel.name, encoded, (_) {});
  }

  Future<void> sendLocalNetworkStateChanged({
    required String state,
  }) async {
    const codec = StandardMethodCodec();
    final encoded = codec.encodeMethodCall(
      MethodCall(methodLocalNetworkStateChanged, <String, dynamic>{
        'state': state,
      }),
    );
    await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
        .handlePlatformMessage(channel.name, encoded, (_) {});
  }

  setUp(() {
    channel = const MethodChannel(macOSPermissionsChannelName);
    outgoingCalls = <MethodCall>[];
    outgoingResponder = null;

    // The mock handler captures outbound calls AND delegates inbound
    // platform messages to the service handler when set.
    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
        .setMockMethodCallHandler(channel, (call) async {
      outgoingCalls.add(call);
      final r = outgoingResponder;
      if (r != null) return r(call);
      return null;
    });
  });

  tearDown(() {
    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
        .setMockMethodCallHandler(channel, null);
  });

  // ===========================================================================
  // Initial state
  // ===========================================================================

  test(
    'SWE4-UV / SRS-297: a fresh service exposes a deterministic initial '
    'inputMonitoringState that matches the host platform',
    () {
      final svc = MacOSPermissionsService(channel: channel);
      if (hostIsMacOS) {
        // On macOS the service hasn't queried the native side yet.
        expect(svc.inputMonitoringState.value, MacOSPermissionState.unknown);
        expect(svc.pttCapabilityState.value, 'L0Focused');
      } else {
        // On non-macOS the static check short-circuits to granted.
        expect(svc.inputMonitoringState.value, MacOSPermissionState.granted);
        expect(svc.pttCapabilityState.value, 'L1MacOSEventTap');
      }
      svc.dispose();
    },
  );

  // ===========================================================================
  // Input Monitoring — inbound state changes
  // ===========================================================================

  test(
    'SWE4-UV / SRS-297: inbound inputMonitoringStateChanged with '
    'state=Granted transitions inputMonitoringState and PTT capability',
    () async {
      final svc = MacOSPermissionsService(channel: channel)..start();

      // Drive away from the initial value first.
      await sendInputMonitoringStateChanged(state: 'Denied');
      expect(svc.inputMonitoringState.value, MacOSPermissionState.denied);
      expect(svc.pttCapabilityState.value, 'L0Focused');

      var notified = 0;
      void listener() => notified++;
      svc.inputMonitoringState.addListener(listener);

      await sendInputMonitoringStateChanged(state: 'Granted');

      expect(svc.inputMonitoringState.value, MacOSPermissionState.granted);
      expect(svc.pttCapabilityState.value, 'L1MacOSEventTap');
      expect(notified, greaterThanOrEqualTo(1));

      svc.inputMonitoringState.removeListener(listener);
      svc.dispose();
    },
  );

  test(
    'SWE4-UV / SRS-297: inbound inputMonitoringStateChanged with '
    'state=Denied transitions to denied and L0Focused',
    () async {
      final svc = MacOSPermissionsService(channel: channel)..start();

      await sendInputMonitoringStateChanged(state: 'Denied');

      expect(svc.inputMonitoringState.value, MacOSPermissionState.denied);
      expect(svc.pttCapabilityState.value, 'L0Focused');
      svc.dispose();
    },
  );

  test(
    'SWE4-UV / SRS-297: inbound inputMonitoringStateChanged with '
    'state=NotDetermined transitions to notDetermined and L0Focused',
    () async {
      final svc = MacOSPermissionsService(channel: channel)..start();

      await sendInputMonitoringStateChanged(state: 'NotDetermined');

      expect(
        svc.inputMonitoringState.value,
        MacOSPermissionState.notDetermined,
      );
      expect(svc.pttCapabilityState.value, 'L0Focused');
      svc.dispose();
    },
  );

  test(
    'SWE4-UV / SRS-297: inbound inputMonitoringStateChanged with '
    'malformed state parses to unknown and L0Focused',
    () async {
      final svc = MacOSPermissionsService(channel: channel)..start();

      // Start from a known baseline.
      await sendInputMonitoringStateChanged(state: 'Granted');
      expect(svc.inputMonitoringState.value, MacOSPermissionState.granted);

      await sendInputMonitoringStateChanged(state: 'Bogus');

      expect(svc.inputMonitoringState.value, MacOSPermissionState.unknown);
      expect(svc.pttCapabilityState.value, 'L0Focused');
      svc.dispose();
    },
  );

  // ===========================================================================
  // Local Network — inbound state changes
  // ===========================================================================

  test(
    'SWE4-UV / SRS-300: inbound localNetworkStateChanged with '
    'state=Granted transitions localNetworkState',
    () async {
      final svc = MacOSPermissionsService(channel: channel)..start();

      await sendLocalNetworkStateChanged(state: 'Granted');

      expect(svc.localNetworkState.value, MacOSLocalNetworkState.granted);
      svc.dispose();
    },
  );

  test(
    'SWE4-UV / SRS-300: inbound localNetworkStateChanged with '
    'state=Denied transitions localNetworkState',
    () async {
      final svc = MacOSPermissionsService(channel: channel)..start();

      await sendLocalNetworkStateChanged(state: 'Denied');

      expect(svc.localNetworkState.value, MacOSLocalNetworkState.denied);
      svc.dispose();
    },
  );

  test(
    'SWE4-UV / SRS-300: inbound localNetworkStateChanged with '
    'state=Unsupported transitions localNetworkState',
    () async {
      final svc = MacOSPermissionsService(channel: channel)..start();

      await sendLocalNetworkStateChanged(state: 'Unsupported');

      expect(
        svc.localNetworkState.value,
        MacOSLocalNetworkState.unsupported,
      );
      svc.dispose();
    },
  );

  // ===========================================================================
  // Outbound method calls
  // ===========================================================================

  test(
    'SWE4-UV / SRS-297: requestInputMonitoring() emits outbound '
    'requestInputMonitoring MethodCall; returns the platform response',
    () async {
      final svc = MacOSPermissionsService(channel: channel)..start();

      outgoingResponder = (call) async {
        if (call.method == methodRequestInputMonitoring) {
          return 'Granted';
        }
        return null;
      };

      final result = await svc.requestInputMonitoring();

      expect(
        outgoingCalls.where((c) => c.method == methodRequestInputMonitoring),
        hasLength(1),
        reason: 'requestInputMonitoring must be called',
      );
      expect(result, MacOSPermissionState.granted);
      expect(svc.inputMonitoringState.value, MacOSPermissionState.granted);
      expect(svc.pttCapabilityState.value, 'L1MacOSEventTap');
      svc.dispose();
    },
  );

  test(
    'SWE4-UV / SRS-300: triggerLocalNetworkPrompt() emits outbound '
    'triggerLocalNetworkPrompt MethodCall; returns the platform response',
    () async {
      final svc = MacOSPermissionsService(channel: channel)..start();

      outgoingResponder = (call) async {
        if (call.method == methodTriggerLocalNetworkPrompt) {
          return 'Granted';
        }
        return null;
      };

      final result = await svc.triggerLocalNetworkPrompt();

      expect(
        outgoingCalls
            .where((c) => c.method == methodTriggerLocalNetworkPrompt),
        hasLength(1),
        reason: 'triggerLocalNetworkPrompt must be called',
      );
      expect(result, MacOSLocalNetworkState.granted);
      svc.dispose();
    },
  );

  test(
    'SWE4-UV / SysRS-166: requestNotifications() emits outbound '
    'requestNotifications MethodCall; returns the platform response',
    () async {
      final svc = MacOSPermissionsService(channel: channel)..start();

      outgoingResponder = (call) async {
        if (call.method == methodRequestNotifications) {
          return 'Granted';
        }
        return null;
      };

      final result = await svc.requestNotifications();

      expect(
        outgoingCalls.where((c) => c.method == methodRequestNotifications),
        hasLength(1),
        reason: 'requestNotifications must be called',
      );
      expect(result, MacOSPermissionState.granted);
      svc.dispose();
    },
  );

  test(
    'SWE4-UV / SRS-297: openInputMonitoringSettings() emits outbound '
    'openInputMonitoringSettings MethodCall',
    () async {
      final svc = MacOSPermissionsService(channel: channel)..start();

      await svc.openInputMonitoringSettings();

      final calls = outgoingCalls
          .where((c) => c.method == methodOpenInputMonitoringSettings)
          .toList();
      expect(calls, hasLength(1));
      svc.dispose();
    },
  );

  // ===========================================================================
  // Lifecycle
  // ===========================================================================

  test(
    'SWE4-UV / SRS-297: start() is idempotent — calling it twice '
    'does not double-register the handler',
    () async {
      final svc = MacOSPermissionsService(channel: channel)
        ..start()
        ..start();

      var notified = 0;
      void listener() => notified++;
      svc.inputMonitoringState.addListener(listener);

      await sendInputMonitoringStateChanged(state: 'Denied');

      expect(svc.inputMonitoringState.value, MacOSPermissionState.denied);
      expect(notified, 1);

      svc.inputMonitoringState.removeListener(listener);
      svc.dispose();
    },
  );

  test(
    'SWE4-UV / SRS-297: stop() removes the handler — subsequent '
    'inbound messages have no effect on state',
    () async {
      final svc = MacOSPermissionsService(channel: channel)..start();

      await sendInputMonitoringStateChanged(state: 'Denied');
      expect(svc.inputMonitoringState.value, MacOSPermissionState.denied);

      svc.stop();

      await sendInputMonitoringStateChanged(state: 'Granted');

      expect(
        svc.inputMonitoringState.value,
        MacOSPermissionState.denied,
        reason: 'state must be frozen after stop()',
      );
    },
  );

  // ===========================================================================
  // Non-macOS short-circuit (channel is null)
  // ===========================================================================

  test(
    'SWE4-UV / SRS-297: null-channel short-circuit — on non-macOS hosts '
    'the constructor seeds to safe defaults; on macOS hosts, passing '
    'channel:null still creates a real channel because _isMacOS is true',
    () async {
      if (!hostIsMacOS) {
        // On non-macOS: the constructor's `_isMacOS` branch is false,
        // so `_channel` stays null. All methods return safe defaults
        // without touching any channel.
        final svc = MacOSPermissionsService(channel: null);
        expect(
          svc.inputMonitoringState.value,
          MacOSPermissionState.granted,
        );
        expect(
          svc.localNetworkState.value,
          MacOSLocalNetworkState.unsupported,
        );
        expect(
          svc.notificationState.value,
          MacOSPermissionState.granted,
        );

        final priorOutgoing = outgoingCalls.length;
        await svc.requestInputMonitoring();
        await svc.triggerLocalNetworkPrompt();
        await svc.requestNotifications();
        await svc.openInputMonitoringSettings();

        expect(
          outgoingCalls.length,
          priorOutgoing,
          reason: 'no outbound calls when platform is non-macOS',
        );

        svc.start();
        svc.stop();
        svc.dispose();
      } else {
        // On macOS: even with channel: null, the constructor creates
        // a MethodChannel because _isMacOS is true. This is the
        // correct production behaviour — on macOS the service always
        // has a channel. The short-circuit path is unreachable on
        // macOS by design.
        final svc = MacOSPermissionsService(channel: null);
        // The service has a non-null _channel, so methods will attempt
        // to invoke the channel (which has no native handler in tests).
        // Verify the service doesn't throw and returns a value.
        final result = await svc.requestInputMonitoring();
        expect(result, isNotNull);
        svc.dispose();
      }
    },
  );

  // ===========================================================================
  // Channel error handling
  // ===========================================================================

  test(
    'SWE4-UV / SRS-297: requestInputMonitoring() returns cached state '
    'when the channel throws',
    () async {
      final svc = MacOSPermissionsService(channel: channel)..start();

      // Seed a known state via inbound.
      await sendInputMonitoringStateChanged(state: 'Denied');
      expect(svc.inputMonitoringState.value, MacOSPermissionState.denied);

      // Make the platform stub throw.
      outgoingResponder = (call) async {
        if (call.method == methodRequestInputMonitoring) {
          throw PlatformException(code: 'unavailable');
        }
        return null;
      };

      final result = await svc.requestInputMonitoring();

      expect(
        result,
        MacOSPermissionState.denied,
        reason:
            'on channel failure requestInputMonitoring must return cached state',
      );
      svc.dispose();
    },
  );

  test(
    'SWE4-UV / SRS-300: triggerLocalNetworkPrompt() returns cached state '
    'when the channel throws',
    () async {
      final svc = MacOSPermissionsService(channel: channel)..start();

      outgoingResponder = (call) async {
        if (call.method == methodTriggerLocalNetworkPrompt) {
          throw PlatformException(code: 'unavailable');
        }
        return null;
      };

      final result = await svc.triggerLocalNetworkPrompt();

      // Returns the cached state without crashing.
      expect(result, isNotNull);
      svc.dispose();
    },
  );
}
