import 'package:flutter_test/flutter_test.dart';
import 'package:nomadcode_app/src/features/workspaces/domain/workspace_task.dart';
import 'package:nomadcode_app/src/integrations/proto_socket/proto_socket_endpoint_config.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';
import 'package:nomadcode_app/src/integrations/proto_socket/proto_socket_task_service.dart';

/// Fake transport that records outgoing envelopes and replays queued responses.
/// No network sockets are opened.
class _FakeTransport implements ProtoSocketTransport {
  final List<ProtoSocketEnvelope> sent = [];
  final List<ProtoSocketEnvelope> _responses = [];

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

  @override
  Future<ProtoSocketEnvelope> sendRequest(
    ProtoSocketEnvelope envelope, {
    Duration timeout = const Duration(seconds: 30),
  }) async {
    sent.add(envelope);
    if (_responses.isEmpty) {
      throw StateError('no queued response for ${envelope.action}');
    }
    return _responses.removeAt(0);
  }

  @override
  Stream<ProtoSocketEnvelope> get events => const Stream.empty();

  @override
  Future<void> close() async {}
}

/// Connector that hands the lifecycle a fixed transport so the task service
/// exercises the real diagnostics wrapper the lifecycle installs.
class _DiagnosticsConnector implements ProtoSocketConnector {
  final _FakeTransport transport;

  _DiagnosticsConnector(this.transport);

  @override
  Future<ProtoSocketTransport> connect(ProtoSocketEndpointConfig config) async =>
      transport;

  @override
  Future<void> disconnect() async {}
}

const _diagnosticsConfig = ProtoSocketEndpointConfig(
  host: 'core.example.com',
  port: 443,
  secure: true,
);

ProtoSocketEnvelope _response(
  String action,
  Map<String, Object?> payload,
) {
  return ProtoSocketEnvelope(
    id: 'resp-$action',
    correlationId: 'req-$action',
    type: 'response',
    channel: 'task',
    action: action,
    payload: payload,
  );
}

ProtoSocketEnvelope _errorResponse(
  String action,
  ProtoSocketEnvelopeError error,
) {
  return ProtoSocketEnvelope(
    id: 'err-$action',
    correlationId: 'req-$action',
    type: 'error',
    channel: 'task',
    action: action,
    error: error,
  );
}

void main() {
  group('WorkspaceTask.fromMap', () {
    test('parses a full task map', () {
      final task = WorkspaceTask.fromMap({
        'id': 'task-1',
        'title': 'Implement task service',
        'status': 'pending',
        'source': 'manual',
        'error': 'boom',
      });

      expect(task.id, equals('task-1'));
      expect(task.title, equals('Implement task service'));
      expect(task.status, equals('pending'));
      expect(task.source, equals('manual'));
      expect(task.error, equals('boom'));
    });

    test('falls back for a minimal task map', () {
      final task = WorkspaceTask.fromMap(const {'id': 'task-2'});

      expect(task.id, equals('task-2'));
      expect(task.title, equals('Untitled task'));
      expect(task.status, equals('unknown'));
      expect(task.source, equals(''));
      expect(task.error, isNull);
    });
  });

  group('ProtoSocketTaskService', () {
    test('listTasks sends {limit} and parses tasks', () async {
      final transport = _FakeTransport()
        ..enqueueResponse(
          _response('task.list', {
            'tasks': [
              {'id': 'task-1', 'title': 'A', 'status': 'pending', 'source': 'manual'},
              {'id': 'task-2', 'title': 'B', 'status': 'queued', 'source': 'plane'},
            ],
          }),
        );
      final service = ProtoSocketTaskService(transport);

      final tasks = await service.listTasks(limit: 5);

      expect(tasks, hasLength(2));
      expect(tasks.first.id, equals('task-1'));
      final request = transport.sent.single;
      expect(request.type, equals('request'));
      expect(request.channel, equals('task'));
      expect(request.action, equals('task.list'));
      expect(request.payload, equals({'limit': 5}));
    });

    test('listTasks returns empty list for malformed tasks payload', () async {
      final transport = _FakeTransport()
        ..enqueueResponse(_response('task.list', {'tasks': 'not-a-list'}));
      final service = ProtoSocketTaskService(transport);

      expect(await service.listTasks(), isEmpty);
    });

    test('getTask sends {id} and parses task', () async {
      final transport = _FakeTransport()
        ..enqueueResponse(
          _response('task.get', {
            'task': {
              'id': 'task-9',
              'title': 'Detail',
              'status': 'running',
              'source': 'manual',
            },
          }),
        );
      final service = ProtoSocketTaskService(transport);

      final task = await service.getTask('task-9');

      expect(task.id, equals('task-9'));
      expect(task.status, equals('running'));
      expect(transport.sent.single.action, equals('task.get'));
      expect(transport.sent.single.payload, equals({'id': 'task-9'}));
    });

    test('enqueueTask returns the updated task from payload.task', () async {
      final transport = _FakeTransport()
        ..enqueueResponse(
          _response('task.enqueue', {
            'id': 'task-3',
            'status': 'queued',
            'task': {
              'id': 'task-3',
              'title': 'Run me',
              'status': 'queued',
              'source': 'manual',
            },
          }),
        );
      final service = ProtoSocketTaskService(transport);

      final task = await service.enqueueTask('task-3');

      expect(task.id, equals('task-3'));
      expect(task.status, equals('queued'));
      expect(transport.sent.single.action, equals('task.enqueue'));
      expect(transport.sent.single.payload, equals({'id': 'task-3'}));
    });

    test('createTask sends documented request shape and parses task', () async {
      final transport = _FakeTransport()
        ..enqueueResponse(
          _response('task.create', {
            'id': 'task-7',
            'status': 'pending',
            'task': {
              'id': 'task-7',
              'title': 'New',
              'status': 'pending',
              'source': 'manual',
            },
          }),
        );
      final service = ProtoSocketTaskService(transport);

      final task = await service.createTask(
        title: 'New',
        source: 'manual',
        payload: {'k': 'v'},
      );

      expect(task.id, equals('task-7'));
      final request = transport.sent.single;
      expect(request.action, equals('task.create'));
      expect(request.payload['title'], equals('New'));
      expect(request.payload['source'], equals('manual'));
      expect(request.payload['payload'], equals({'k': 'v'}));
    });

    test('error envelope maps to ProtoSocketTaskException with retryable', () async {
      final transport = _FakeTransport()
        ..enqueueResponse(
          _errorResponse(
            'task.get',
            const ProtoSocketEnvelopeError(
              code: 'task.not_found',
              message: 'task not found',
              retryable: false,
            ),
          ),
        );
      final service = ProtoSocketTaskService(transport);

      await expectLater(
        service.getTask('missing'),
        throwsA(
          isA<ProtoSocketTaskException>()
              .having((e) => e.code, 'code', 'task.not_found')
              .having((e) => e.message, 'message', 'task not found')
              .having((e) => e.retryable, 'retryable', isFalse),
        ),
      );
    });

    test('retryable internal error preserves retryable flag', () async {
      final transport = _FakeTransport()
        ..enqueueResponse(
          _errorResponse(
            'task.list',
            const ProtoSocketEnvelopeError(
              code: 'internal.error',
              message: 'internal server error',
              retryable: true,
            ),
          ),
        );
      final service = ProtoSocketTaskService(transport);

      await expectLater(
        service.listTasks(),
        throwsA(
          isA<ProtoSocketTaskException>()
              .having((e) => e.retryable, 'retryable', isTrue),
        ),
      );
    });

    test('getTask throws service exception for malformed success payload', () async {
      final transport = _FakeTransport()
        ..enqueueResponse(_response('task.get', {'task': 'not-a-map'}));
      final service = ProtoSocketTaskService(transport);

      await expectLater(
        service.getTask('task-x'),
        throwsA(
          isA<ProtoSocketTaskException>()
              .having((e) => e.code, 'code', 'client.malformed_response'),
        ),
      );
    });

    test('records lifecycle diagnostics on successful task response', () async {
      final transport = _FakeTransport()
        ..enqueueResponse(
          ProtoSocketEnvelope(
            id: 'response-1',
            type: 'response',
            channel: 'task',
            action: 'task.list',
            payload: const {
              'tasks': [
                {
                  'id': 'task-1',
                  'title': 'A',
                  'status': 'running',
                  'source': 'core',
                },
              ],
            },
            meta: const {
              'connection_id': 'conn-task',
              'timestamp': '2026-05-30T12:00:00Z',
            },
          ),
        );
      final lifecycle = ProtoSocketLifecycle(
        connector: _DiagnosticsConnector(transport),
      );
      await lifecycle.connect(_diagnosticsConfig);
      final service = ProtoSocketTaskService(lifecycle.transport!);

      final tasks = await service.listTasks();

      expect(tasks, hasLength(1));
      expect(lifecycle.diagnostics.connectionId, equals('conn-task'));
      expect(lifecycle.diagnostics.channel, equals('task'));
      expect(lifecycle.diagnostics.action, equals('task.list'));
      expect(
        lifecycle.diagnostics.protocolVersion,
        equals(protoSocketProtocolVersion),
      );
      expect(lifecycle.diagnostics.errorCode, isNull);
      expect(lifecycle.diagnostics.timestamp, equals('2026-05-30T12:00:00Z'));

      await lifecycle.dispose();
    });

    test('records error code while still throwing on error envelope', () async {
      final transport = _FakeTransport()
        ..enqueueResponse(
          const ProtoSocketEnvelope(
            id: 'err-1',
            type: 'response',
            channel: 'task',
            action: 'task.list',
            error: ProtoSocketEnvelopeError(
              code: 'task.unavailable',
              message: 'tasks temporarily unavailable',
              retryable: true,
            ),
            meta: {'connection_id': 'conn-task-err'},
          ),
        );
      final lifecycle = ProtoSocketLifecycle(
        connector: _DiagnosticsConnector(transport),
      );
      await lifecycle.connect(_diagnosticsConfig);
      final service = ProtoSocketTaskService(lifecycle.transport!);

      await expectLater(
        service.listTasks(),
        throwsA(isA<ProtoSocketTaskException>()),
      );

      // The diagnostics wrapper captures the error code from the envelope even
      // though the service maps it to a thrown exception.
      expect(lifecycle.diagnostics.errorCode, equals('task.unavailable'));
      expect(lifecycle.diagnostics.connectionId, equals('conn-task-err'));

      await lifecycle.dispose();
    });
  });
}
