import 'dart:async';

import 'package:flutter_test/flutter_test.dart';
import 'package:nomadcode_app/src/integrations/proto_socket/proto_socket_branch_event_service.dart';
import 'package:nomadcode_app/src/integrations/proto_socket/proto_socket_envelope.dart';
import 'package:nomadcode_app/src/integrations/proto_socket/proto_socket_lifecycle.dart';

class _FakeTransport implements ProtoSocketTransport {
  final List<ProtoSocketEnvelope> sent = [];
  final List<ProtoSocketEnvelope> _responses = [];
  final StreamController<ProtoSocketEnvelope> _events =
      StreamController<ProtoSocketEnvelope>.broadcast();

  void enqueueResponse(ProtoSocketEnvelope response) {
    _responses.add(response);
  }

  void emitEvent(ProtoSocketEnvelope event) {
    _events.add(event);
  }

  @override
  Future<ProtoSocketEnvelope> sendRequest(
    ProtoSocketEnvelope envelope, {
    Duration timeout = const Duration(seconds: 30),
  }) async {
    sent.add(envelope);
    if (_responses.isEmpty) {
      return ProtoSocketEnvelope(
        id: 'resp-${envelope.id}',
        correlationId: envelope.id,
        type: 'response',
        channel: envelope.channel,
        action: envelope.action,
        payload: const {'status': 'subscribed'},
      );
    }
    return _responses.removeAt(0);
  }

  @override
  Stream<ProtoSocketEnvelope> get events => _events.stream;

  @override
  Future<void> close() async {
    await _events.close();
  }
}

void main() {
  group('ProtoSocketBranchEventService', () {
    late _FakeTransport transport;
    late ProtoSocketBranchEventService service;

    setUp(() {
      transport = _FakeTransport();
      service = ProtoSocketBranchEventService(transport);
    });

    tearDown(() async {
      service.dispose();
      await transport.close();
    });

    test('subscribe sends event.subscribe request with correct shape', () async {
      await service.subscribe(repoId: 'nomadcode', branch: 'develop');

      expect(transport.sent, hasLength(1));
      final request = transport.sent.first;
      expect(request.type, equals('request'));
      expect(request.channel, equals('event'));
      expect(request.action, equals('event.subscribe'));
      expect(request.payload['events'], equals(['branch.updated']));
      expect(request.payload['repo_id'], equals('nomadcode'));
      expect(request.payload['branch'], equals('develop'));
    });

    test('emits branch.updated as wakeup signal when event is received', () async {
      // Must subscribe first to pass the repo/branch filter
      await service.subscribe(repoId: 'nomadcode', branch: 'develop');

      final eventsFuture = service.branchUpdates.first;

      transport.emitEvent(const ProtoSocketEnvelope(
        id: 'event-1',
        type: 'event',
        channel: 'event',
        action: 'branch.updated',
        payload: {
          'id': 'event-1',
          'type': 'branch.updated',
          'provider': 'forgejo',
          'delivery_id': 'delivery-1',
          'repo_id': 'nomadcode',
          'branch': 'develop',
          'before': 'old-sha',
          'after': 'new-sha',
          'changed_files': [
            {'path': 'README.md', 'change_type': 'modified'}
          ],
          'observed_at': '2026-06-13T00:00:00Z',
          'created_at': '2026-06-13T00:00:00Z'
        },
      ));

      final event = await eventsFuture;
      expect(event.id, equals('event-1'));
      expect(event.type, equals('branch.updated'));
      expect(event.provider, equals('forgejo'));
      expect(event.deliveryId, equals('delivery-1'));
      expect(event.repoId, equals('nomadcode'));
      expect(event.branch, equals('develop'));
      expect(event.before, equals('old-sha'));
      expect(event.after, equals('new-sha'));
      expect(event.changedFiles, hasLength(1));
      expect(event.changedFiles.first.path, equals('README.md'));
      expect(event.changedFiles.first.changeType, equals('modified'));
      expect(event.observedAt, equals('2026-06-13T00:00:00Z'));
      expect(event.createdAt, equals('2026-06-13T00:00:00Z'));
    });

    test('ignores non-matching event channel or actions', () async {
      await service.subscribe(repoId: 'nomadcode', branch: 'develop');

      final events = <BranchUpdatedEvent>[];
      final subscription = service.branchUpdates.listen(events.add);

      // Non-matching action
      transport.emitEvent(const ProtoSocketEnvelope(
        id: 'event-2',
        type: 'event',
        channel: 'event',
        action: 'branch.created',
        payload: {},
      ));

      // Non-matching channel
      transport.emitEvent(const ProtoSocketEnvelope(
        id: 'event-3',
        type: 'event',
        channel: 'task',
        action: 'branch.updated',
        payload: {},
      ));

      // Non-matching type
      transport.emitEvent(const ProtoSocketEnvelope(
        id: 'event-4',
        type: 'response',
        channel: 'event',
        action: 'branch.updated',
        payload: {},
      ));

      await Future.delayed(const Duration(milliseconds: 10));

      expect(events, isEmpty);
      await subscription.cancel();
    });

    test('emits error on stream when event payload is malformed', () async {
      await service.subscribe(repoId: 'nomadcode', branch: 'develop');

      final events = <BranchUpdatedEvent>[];
      final errors = [];

      final subscription = service.branchUpdates.listen(
        events.add,
        onError: errors.add,
      );

      // Missing 'repo_id' in payload
      transport.emitEvent(const ProtoSocketEnvelope(
        id: 'event-5',
        type: 'event',
        channel: 'event',
        action: 'branch.updated',
        payload: {
          'id': 'event-5',
          'type': 'branch.updated',
          'provider': 'forgejo',
          'delivery_id': 'delivery-1',
          'branch': 'develop',
          'before': 'old-sha',
          'after': 'new-sha',
          'changed_files': [],
          'observed_at': '2026-06-13T00:00:00Z',
          'created_at': '2026-06-13T00:00:00Z'
        },
      ));

      await Future.delayed(const Duration(milliseconds: 10));

      expect(events, isEmpty);
      expect(errors, hasLength(1));
      expect(errors.first, isA<FormatException>());

      await subscription.cancel();
    });

    test('subscribe throws when Core returns an error envelope', () async {
      transport.enqueueResponse(const ProtoSocketEnvelope(
        id: 'resp-msg-1',
        correlationId: 'msg-1',
        type: 'error',
        channel: 'event',
        action: 'event.subscribe',
        error: ProtoSocketEnvelopeError(
          code: 'event.missing_connection',
          message: 'active connection missing',
          retryable: false,
        ),
      ));

      await expectLater(
        service.subscribe(repoId: 'nomadcode', branch: 'develop'),
        throwsA(
          isA<ProtoSocketBranchEventException>()
              .having((e) => e.code, 'code', 'event.missing_connection')
              .having((e) => e.message, 'message', 'active connection missing')
              .having((e) => e.retryable, 'retryable', isFalse),
        ),
      );

      // Verify that filter was not added, and subsequent event is ignored
      final events = <BranchUpdatedEvent>[];
      final subscription = service.branchUpdates.listen(events.add);

      transport.emitEvent(const ProtoSocketEnvelope(
        id: 'event-6',
        type: 'event',
        channel: 'event',
        action: 'branch.updated',
        payload: {
          'id': 'event-6',
          'type': 'branch.updated',
          'provider': 'forgejo',
          'delivery_id': 'delivery-1',
          'repo_id': 'nomadcode',
          'branch': 'develop',
          'before': 'old-sha',
          'after': 'new-sha',
          'changed_files': [],
          'observed_at': '2026-06-13T00:00:00Z',
          'created_at': '2026-06-13T00:00:00Z'
        },
      ));

      await Future.delayed(const Duration(milliseconds: 10));
      expect(events, isEmpty);
      await subscription.cancel();
    });

    test('emits only subscribed repo and branch events', () async {
      // 1. Before subscribe: no events should be emitted (filter set is empty)
      final eventsBefore = <BranchUpdatedEvent>[];
      final subBefore = service.branchUpdates.listen(eventsBefore.add);

      transport.emitEvent(const ProtoSocketEnvelope(
        id: 'event-before',
        type: 'event',
        channel: 'event',
        action: 'branch.updated',
        payload: {
          'id': 'event-before',
          'type': 'branch.updated',
          'provider': 'forgejo',
          'delivery_id': 'delivery-1',
          'repo_id': 'nomadcode',
          'branch': 'develop',
          'before': 'old-sha',
          'after': 'new-sha',
          'changed_files': [],
          'observed_at': '2026-06-13T00:00:00Z',
          'created_at': '2026-06-13T00:00:00Z'
        },
      ));

      await Future.delayed(const Duration(milliseconds: 10));
      expect(eventsBefore, isEmpty);
      await subBefore.cancel();

      // 2. Subscribe to nomadcode/develop
      await service.subscribe(repoId: 'nomadcode', branch: 'develop');

      final eventsAfter = <BranchUpdatedEvent>[];
      final subAfter = service.branchUpdates.listen(eventsAfter.add);

      // Event for other repo
      transport.emitEvent(const ProtoSocketEnvelope(
        id: 'event-other-repo',
        type: 'event',
        channel: 'event',
        action: 'branch.updated',
        payload: {
          'id': 'event-other-repo',
          'type': 'branch.updated',
          'provider': 'forgejo',
          'delivery_id': 'delivery-1',
          'repo_id': 'other-repo',
          'branch': 'develop',
          'before': 'old-sha',
          'after': 'new-sha',
          'changed_files': [],
          'observed_at': '2026-06-13T00:00:00Z',
          'created_at': '2026-06-13T00:00:00Z'
        },
      ));

      // Event for other branch
      transport.emitEvent(const ProtoSocketEnvelope(
        id: 'event-other-branch',
        type: 'event',
        channel: 'event',
        action: 'branch.updated',
        payload: {
          'id': 'event-other-branch',
          'type': 'branch.updated',
          'provider': 'forgejo',
          'delivery_id': 'delivery-1',
          'repo_id': 'nomadcode',
          'branch': 'main',
          'before': 'old-sha',
          'after': 'new-sha',
          'changed_files': [],
          'observed_at': '2026-06-13T00:00:00Z',
          'created_at': '2026-06-13T00:00:00Z'
        },
      ));

      // Matching event
      transport.emitEvent(const ProtoSocketEnvelope(
        id: 'event-matching',
        type: 'event',
        channel: 'event',
        action: 'branch.updated',
        payload: {
          'id': 'event-matching',
          'type': 'branch.updated',
          'provider': 'forgejo',
          'delivery_id': 'delivery-1',
          'repo_id': 'nomadcode',
          'branch': 'develop',
          'before': 'old-sha',
          'after': 'new-sha',
          'changed_files': [],
          'observed_at': '2026-06-13T00:00:00Z',
          'created_at': '2026-06-13T00:00:00Z'
        },
      ));

      await Future.delayed(const Duration(milliseconds: 10));

      expect(eventsAfter, hasLength(1));
      expect(eventsAfter.first.id, equals('event-matching'));
      await subAfter.cancel();
    });

    test('emits error when changed_files contains a malformed item', () async {
      await service.subscribe(repoId: 'nomadcode', branch: 'develop');

      final events = <BranchUpdatedEvent>[];
      final errors = [];

      final subscription = service.branchUpdates.listen(
        events.add,
        onError: errors.add,
      );

      // changed_files contains a string (not a map)
      transport.emitEvent(const ProtoSocketEnvelope(
        id: 'event-malformed-files-1',
        type: 'event',
        channel: 'event',
        action: 'branch.updated',
        payload: {
          'id': 'event-malformed-files-1',
          'type': 'branch.updated',
          'provider': 'forgejo',
          'delivery_id': 'delivery-1',
          'repo_id': 'nomadcode',
          'branch': 'develop',
          'before': 'old-sha',
          'after': 'new-sha',
          'changed_files': ['not-a-map'],
          'observed_at': '2026-06-13T00:00:00Z',
          'created_at': '2026-06-13T00:00:00Z'
        },
      ));

      // changed_files contains map but missing field 'change_type'
      transport.emitEvent(const ProtoSocketEnvelope(
        id: 'event-malformed-files-2',
        type: 'event',
        channel: 'event',
        action: 'branch.updated',
        payload: {
          'id': 'event-malformed-files-2',
          'type': 'branch.updated',
          'provider': 'forgejo',
          'delivery_id': 'delivery-1',
          'repo_id': 'nomadcode',
          'branch': 'develop',
          'before': 'old-sha',
          'after': 'new-sha',
          'changed_files': [
            {'path': 'README.md'} // change_type is missing
          ],
          'observed_at': '2026-06-13T00:00:00Z',
          'created_at': '2026-06-13T00:00:00Z'
        },
      ));

      await Future.delayed(const Duration(milliseconds: 10));

      expect(events, isEmpty);
      expect(errors, hasLength(2));
      expect(errors[0], isA<FormatException>());
      expect(errors[1], isA<FormatException>());

      await subscription.cancel();
    });
  });
}
