import 'dart:async';
import 'dart:io';
import 'package:fixnum/fixnum.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:proto_socket/proto_socket.dart';

import 'package:alt_client/src/generated/alt/v1/backtest.pb.dart';
import 'package:alt_client/src/generated/alt/v1/common.pb.dart';
import 'package:alt_client/src/generated/alt/v1/market.pb.dart';
import 'package:alt_client/src/integrations/socket/alt_socket_client.dart';
import 'package:alt_client/src/integrations/socket/socket_endpoint.dart';

class FakeWebSocket extends Stream<dynamic> implements WebSocket {
  final StreamController<dynamic> _controller = StreamController<dynamic>();
  final List<List<int>> sentBytes = [];
  bool isClosed = false;

  @override
  void add(dynamic data) {
    if (data is List<int>) {
      sentBytes.add(data);
    }
  }

  @override
  Future<void> close([int? code, String? reason]) async {
    isClosed = true;
    await _controller.close();
  }

  void feedFromServer(List<int> data) {
    _controller.add(data);
  }

  @override
  StreamSubscription<dynamic> listen(
    void Function(dynamic event)? onData, {
    Function? onError,
    void Function()? onDone,
    bool? cancelOnError,
  }) {
    return _controller.stream.listen(
      onData,
      onError: onError,
      onDone: onDone,
      cancelOnError: cancelOnError,
    );
  }

  @override
  dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}

void main() {
  group('AltSocketEndpoint tests', () {
    test('default constructor values', () {
      const endpoint = AltSocketEndpoint();
      expect(endpoint.host, equals('127.0.0.1'));
      expect(endpoint.port, equals(8080));
      expect(endpoint.path, equals('/socket'));
      expect(endpoint.toString(), equals('ws://127.0.0.1:8080/socket'));
    });

    test('custom values', () {
      const endpoint = AltSocketEndpoint(
        host: 'api.example.com',
        port: 443,
        path: '/ws',
      );
      expect(endpoint.host, equals('api.example.com'));
      expect(endpoint.port, equals(443));
      expect(endpoint.path, equals('/ws'));
      expect(endpoint.toString(), equals('ws://api.example.com:443/ws'));
    });

    test('equality and hashCode', () {
      const e1 = AltSocketEndpoint(host: '1.2.3.4', port: 9000, path: '/a');
      const e2 = AltSocketEndpoint(host: '1.2.3.4', port: 9000, path: '/a');
      const e3 = AltSocketEndpoint(host: '1.2.3.4', port: 9000, path: '/b');

      expect(e1, equals(e2));
      expect(e1.hashCode, equals(e2.hashCode));
      expect(e1, isNot(equals(e3)));
    });
  });

  group('AltSocketClient tests', () {
    late FakeWebSocket fakeWs;
    late AltSocketClient client;

    setUp(() {
      fakeWs = FakeWebSocket();
      client = AltSocketClient(
        fakeWs,
        heartbeatIntervalTime: 1,
        heartbeatWaitTime: 1,
      );
    });

    tearDown(() async {
      await client.close();
    });

    test('client initialization and parser registration', () {
      expect(client.isAlive, isTrue);

      final helloRequestType =
          HelloRequest.getDefault().info_.qualifiedMessageName;
      final helloResponseType =
          HelloResponse.getDefault().info_.qualifiedMessageName;

      expect(client.getGenerator<HelloRequest>(helloRequestType), isNotNull);
      expect(client.getGenerator<HelloResponse>(helloResponseType), isNotNull);
    });

    test('hello handshake request-response loop', () async {
      final helloFuture = client.hello(timeout: const Duration(seconds: 1));

      await Future<void>.delayed(Duration.zero);

      expect(fakeWs.sentBytes, isNotEmpty);
      final firstSentPacketBytes = fakeWs.sentBytes.first;
      final sentPacket = PacketBase.fromBuffer(firstSentPacketBytes);

      final helloRequestType =
          HelloRequest.getDefault().info_.qualifiedMessageName;
      expect(sentPacket.typeName, equals(helloRequestType));

      final sentHelloRequest = HelloRequest.fromBuffer(sentPacket.data);
      expect(sentHelloRequest.clientName, equals('alt-client'));
      expect(sentHelloRequest.clientVersion, equals('dev'));
      expect(sentHelloRequest.altProtocolVersion, equals('alt.v1'));

      final expectedResponse = HelloResponse()
        ..serverName = 'alt-server'
        ..serverVersion = '1.0.0'
        ..altProtocolVersion = 'alt.v1';

      final responsePacket = PacketBase()
        ..typeName = HelloResponse.getDefault().info_.qualifiedMessageName
        ..nonce = 100
        ..responseNonce = sentPacket.nonce
        ..data = expectedResponse.writeToBuffer();

      fakeWs.feedFromServer(responsePacket.writeToBuffer());

      final response = await helloFuture;

      expect(response.serverName, equals('alt-server'));
      expect(response.serverVersion, equals('1.0.0'));
      expect(response.altProtocolVersion, equals('alt.v1'));
    });

    // Drains the request sent by [trigger], asserts the sent packet type/payload
    // through [onSent], then feeds a matching response packet back so the worker
    // -> API -> client backtest boundary round-trips consistently.
    Future<PacketBase> roundTrip({
      required Future<void> Function() trigger,
      required void Function(PacketBase sentPacket) onSent,
      required String responseTypeName,
      required List<int> responseData,
    }) async {
      final sentCountBefore = fakeWs.sentBytes.length;
      unawaited(trigger());

      await Future<void>.delayed(Duration.zero);

      expect(fakeWs.sentBytes.length, greaterThan(sentCountBefore));
      final sentPacket = PacketBase.fromBuffer(fakeWs.sentBytes.last);
      onSent(sentPacket);

      final responsePacket = PacketBase()
        ..typeName = responseTypeName
        ..nonce = 200
        ..responseNonce = sentPacket.nonce
        ..data = responseData;
      fakeWs.feedFromServer(responsePacket.writeToBuffer());

      return sentPacket;
    }

    test('listBacktestRuns request-response loop', () async {
      final request = ListBacktestRunsRequest()
        ..status = BacktestRunStatus.BACKTEST_RUN_STATUS_SUCCEEDED;
      final expectedResponse = ListBacktestRunsResponse()
        ..runs.add(BacktestRun()
          ..id = 'run-1'
          ..status = BacktestRunStatus.BACKTEST_RUN_STATUS_SUCCEEDED);

      final future = client.listBacktestRuns(
        request,
        timeout: const Duration(seconds: 1),
      );

      await roundTrip(
        trigger: () => future,
        onSent: (sentPacket) {
          expect(
            sentPacket.typeName,
            equals(
              ListBacktestRunsRequest.getDefault().info_.qualifiedMessageName,
            ),
          );
          final sentRequest = ListBacktestRunsRequest.fromBuffer(sentPacket.data);
          expect(
            sentRequest.status,
            equals(BacktestRunStatus.BACKTEST_RUN_STATUS_SUCCEEDED),
          );
        },
        responseTypeName:
            ListBacktestRunsResponse.getDefault().info_.qualifiedMessageName,
        responseData: expectedResponse.writeToBuffer(),
      );

      final response = await future;
      expect(response.runs, hasLength(1));
      expect(response.runs.first.id, equals('run-1'));
      expect(
        response.runs.first.status,
        equals(BacktestRunStatus.BACKTEST_RUN_STATUS_SUCCEEDED),
      );
    });

    test('getBacktestRunDetail request-response loop', () async {
      final request = GetBacktestRunDetailRequest()..runId = 'run-7';
      final expectedResponse = GetBacktestRunDetailResponse()
        ..run = (BacktestRun()..id = 'run-7')
        ..result = (BacktestResult()..runId = 'run-7');

      final future = client.getBacktestRunDetail(
        request,
        timeout: const Duration(seconds: 1),
      );

      await roundTrip(
        trigger: () => future,
        onSent: (sentPacket) {
          expect(
            sentPacket.typeName,
            equals(
              GetBacktestRunDetailRequest.getDefault()
                  .info_
                  .qualifiedMessageName,
            ),
          );
          final sentRequest =
              GetBacktestRunDetailRequest.fromBuffer(sentPacket.data);
          expect(sentRequest.runId, equals('run-7'));
        },
        responseTypeName: GetBacktestRunDetailResponse.getDefault()
            .info_
            .qualifiedMessageName,
        responseData: expectedResponse.writeToBuffer(),
      );

      final response = await future;
      expect(response.run.id, equals('run-7'));
      expect(response.result.runId, equals('run-7'));
    });

    test('getBacktestResult request-response loop', () async {
      final request = GetBacktestResultRequest()..runId = 'run-9';
      final expectedResponse = GetBacktestResultResponse()
        ..result = (BacktestResult()..runId = 'run-9');

      final future = client.getBacktestResult(
        request,
        timeout: const Duration(seconds: 1),
      );

      await roundTrip(
        trigger: () => future,
        onSent: (sentPacket) {
          expect(
            sentPacket.typeName,
            equals(
              GetBacktestResultRequest.getDefault().info_.qualifiedMessageName,
            ),
          );
          final sentRequest =
              GetBacktestResultRequest.fromBuffer(sentPacket.data);
          expect(sentRequest.runId, equals('run-9'));
        },
        responseTypeName:
            GetBacktestResultResponse.getDefault().info_.qualifiedMessageName,
        responseData: expectedResponse.writeToBuffer(),
      );

      final response = await future;
      expect(response.result.runId, equals('run-9'));
    });

    test('compareBacktestRuns request-response loop', () async {
      final request = CompareBacktestRunsRequest()
        ..runIds.addAll(['run-1', 'run-2']);
      final expectedResponse = CompareBacktestRunsResponse()
        ..results.add(BacktestResult()..runId = 'run-1')
        ..results.add(BacktestResult()..runId = 'run-2');

      final future = client.compareBacktestRuns(
        request,
        timeout: const Duration(seconds: 1),
      );

      await roundTrip(
        trigger: () => future,
        onSent: (sentPacket) {
          expect(
            sentPacket.typeName,
            equals(
              CompareBacktestRunsRequest.getDefault().info_.qualifiedMessageName,
            ),
          );
          final sentRequest =
              CompareBacktestRunsRequest.fromBuffer(sentPacket.data);
          expect(sentRequest.runIds, equals(['run-1', 'run-2']));
        },
        responseTypeName:
            CompareBacktestRunsResponse.getDefault().info_.qualifiedMessageName,
        responseData: expectedResponse.writeToBuffer(),
      );

      final response = await future;
      expect(response.results, hasLength(2));
      expect(
        response.results.map((r) => r.runId),
        equals(['run-1', 'run-2']),
      );
    });

    test('startBacktest command request-response loop', () async {
      final request = StartBacktestRequest()
        ..spec = (BacktestRunSpec()
          ..strategyId = 'strategy-1'
          ..market = Market.MARKET_KR
          ..timeframe = Timeframe.TIMEFRAME_DAILY
          ..fromUnixMs = Int64(1000)
          ..toUnixMs = Int64(2000));
      final expectedResponse = StartBacktestResponse()
        ..run = (BacktestRun()
          ..id = 'run-new'
          ..status = BacktestRunStatus.BACKTEST_RUN_STATUS_PENDING);

      final future = client.startBacktest(
        request,
        timeout: const Duration(seconds: 1),
      );

      await roundTrip(
        trigger: () => future,
        onSent: (sentPacket) {
          expect(
            sentPacket.typeName,
            equals(
              StartBacktestRequest.getDefault().info_.qualifiedMessageName,
            ),
          );
          final sentRequest = StartBacktestRequest.fromBuffer(sentPacket.data);
          expect(sentRequest.spec.strategyId, equals('strategy-1'));
          expect(sentRequest.spec.market, equals(Market.MARKET_KR));
          expect(
            sentRequest.spec.timeframe,
            equals(Timeframe.TIMEFRAME_DAILY),
          );
        },
        responseTypeName:
            StartBacktestResponse.getDefault().info_.qualifiedMessageName,
        responseData: expectedResponse.writeToBuffer(),
      );

      final response = await future;
      expect(response.run.id, equals('run-new'));
      expect(
        response.run.status,
        equals(BacktestRunStatus.BACKTEST_RUN_STATUS_PENDING),
      );
    });

    test('listInstruments market query request-response loop', () async {
      final request = ListInstrumentsRequest()
        ..market = Market.MARKET_US
        ..provider = 'kis';
      final expectedResponse = ListInstrumentsResponse()
        ..instruments.add(Instrument()
          ..id = 'instrument-1'
          ..market = Market.MARKET_US
          ..symbol = 'AAPL');

      final future = client.listInstruments(
        request,
        timeout: const Duration(seconds: 1),
      );

      await roundTrip(
        trigger: () => future,
        onSent: (sentPacket) {
          expect(
            sentPacket.typeName,
            equals(
              ListInstrumentsRequest.getDefault().info_.qualifiedMessageName,
            ),
          );
          final sentRequest =
              ListInstrumentsRequest.fromBuffer(sentPacket.data);
          expect(sentRequest.market, equals(Market.MARKET_US));
          expect(sentRequest.provider, equals('kis'));
        },
        responseTypeName:
            ListInstrumentsResponse.getDefault().info_.qualifiedMessageName,
        responseData: expectedResponse.writeToBuffer(),
      );

      final response = await future;
      expect(response.instruments, hasLength(1));
      expect(response.instruments.first.id, equals('instrument-1'));
      expect(response.instruments.first.symbol, equals('AAPL'));
    });

    test('listBars market query request-response loop', () async {
      final request = ListBarsRequest()
        ..instrumentId = 'instrument-1'
        ..timeframe = Timeframe.TIMEFRAME_DAILY
        ..fromUnixMs = Int64(1000)
        ..toUnixMs = Int64(2000);
      final expectedResponse = ListBarsResponse()
        ..bars.add(Bar()
          ..instrumentId = 'instrument-1'
          ..timeframe = Timeframe.TIMEFRAME_DAILY
          ..timestampUnixMs = Int64(1500));

      final future = client.listBars(
        request,
        timeout: const Duration(seconds: 1),
      );

      await roundTrip(
        trigger: () => future,
        onSent: (sentPacket) {
          expect(
            sentPacket.typeName,
            equals(
              ListBarsRequest.getDefault().info_.qualifiedMessageName,
            ),
          );
          final sentRequest = ListBarsRequest.fromBuffer(sentPacket.data);
          expect(sentRequest.instrumentId, equals('instrument-1'));
          expect(
            sentRequest.timeframe,
            equals(Timeframe.TIMEFRAME_DAILY),
          );
        },
        responseTypeName:
            ListBarsResponse.getDefault().info_.qualifiedMessageName,
        responseData: expectedResponse.writeToBuffer(),
      );

      final response = await future;
      expect(response.bars, hasLength(1));
      expect(response.bars.first.instrumentId, equals('instrument-1'));
      expect(
        response.bars.first.timeframe,
        equals(Timeframe.TIMEFRAME_DAILY),
      );
    });

    // Feeds a response carrying the wrong message type for the pending request
    // so the typed `sendRequest` boundary surfaces a StateError instead of
    // silently completing. Verifies the new API wrappers propagate errors the
    // same way the existing backtest query wrappers do.
    test('new API wrappers propagate response type mismatch errors', () async {
      Future<void> expectTypeMismatch(Future<dynamic> future) async {
        final sentCountBefore = fakeWs.sentBytes.length;
        unawaited(future);

        await Future<void>.delayed(Duration.zero);

        expect(fakeWs.sentBytes.length, greaterThan(sentCountBefore));
        final sentPacket = PacketBase.fromBuffer(fakeWs.sentBytes.last);

        final wrongResponse = HelloResponse()..serverName = 'wrong-type';
        final responsePacket = PacketBase()
          ..typeName = HelloResponse.getDefault().info_.qualifiedMessageName
          ..nonce = 300
          ..responseNonce = sentPacket.nonce
          ..data = wrongResponse.writeToBuffer();
        fakeWs.feedFromServer(responsePacket.writeToBuffer());

        await expectLater(future, throwsA(isA<StateError>()));
      }

      await expectTypeMismatch(
        client.startBacktest(
          StartBacktestRequest(),
          timeout: const Duration(seconds: 1),
        ),
      );
      await expectTypeMismatch(
        client.listInstruments(
          ListInstrumentsRequest(),
          timeout: const Duration(seconds: 1),
        ),
      );
      await expectTypeMismatch(
        client.listBars(
          ListBarsRequest(),
          timeout: const Duration(seconds: 1),
        ),
      );
    });
  });
}
