import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter_test/flutter_test.dart';
import 'package:iop_client/control_plane_status_controller.dart';
import 'package:iop_client/control_plane_status_client.dart';
import 'support/client_test_harness.dart';

class TrackingHttpClient extends http.BaseClient {
  int requestCount = 0;
  int closeCount = 0;

  @override
  Future<http.StreamedResponse> send(http.BaseRequest request) async {
    requestCount += 1;
    final response = http.Response(
      json.encode({'edges': <Map<String, dynamic>>[]}),
      200,
    );
    return http.StreamedResponse(
      Stream<List<int>>.fromIterable([response.bodyBytes]),
      response.statusCode,
      headers: response.headers,
      request: request,
    );
  }

  @override
  void close() {
    closeCount += 1;
    super.close();
  }
}

class ControllableStatusRepository implements ControlPlaneStatusRepository {
  final fleetCompleter = Completer<FleetStatusResponseView>();
  final statusCompleter = Completer<EdgeStatusResponseView>();
  final eventsCompleter = Completer<List<EdgeNodeEventView>>();

  int fleetCallCount = 0;
  int statusCallCount = 0;
  int eventsCallCount = 0;

  @override
  Future<FleetStatusResponseView> fetchFleetStatus() {
    fleetCallCount += 1;
    return fleetCompleter.future;
  }

  @override
  Future<EdgeStatusResponseView> fetchEdgeStatus(String edgeId) {
    statusCallCount += 1;
    return statusCompleter.future;
  }

  @override
  Future<List<EdgeNodeEventView>> fetchEdgeEvents(String edgeId) {
    eventsCallCount += 1;
    return eventsCompleter.future;
  }

  @override
  Future<List<EdgeRegistryView>> fetchEdges() async => [];

  @override
  Future<EdgeOperationsResponseView> fetchEdgeOperations(String edgeId) async {
    return EdgeOperationsResponseView(edgeId: edgeId, operations: []);
  }

  @override
  Future<EdgeCommandResponseView> sendEdgeCommand(
    String edgeId,
    String operation, {
    String? targetSelector,
    Map<String, String>? parameters,
  }) async {
    return EdgeCommandResponseView(
      requestId: 'req-1',
      commandId: 'cmd-1',
      edgeId: edgeId,
      status: 'success',
      summary: '',
      error: '',
    );
  }
}

void main() {
  test('HTTP repository keeps injected client caller-owned', () async {
    final client = TrackingHttpClient();
    final repository = HttpControlPlaneStatusRepository(
      baseUrl: 'http://control-plane.test',
      client: client,
    );

    await repository.fetchEdges();
    repository.close();

    expect(client.requestCount, 1);
    expect(client.closeCount, 0);
  });

  test(
    'removes stale selected edge caches when fleet refresh drops edge',
    () async {
      final fakeStatusRepo = FakeControlPlaneStatusRepository();
      final controller = ControlPlaneStatusController(
        statusRepository: fakeStatusRepo,
      );

      // Initial fetch (edge-a and edge-b)
      await controller.fetchEdges();
      expect(controller.edges.length, 2);
      expect(controller.selectedEdgeId, 'edge-a');

      // Select edge-b and fetch details
      controller.selectEdge('edge-b');
      await controller.fetchEdgeDetails('edge-b');

      expect(controller.selectedEdgeId, 'edge-b');
      expect(controller.edgeStatuses.containsKey('edge-b'), true);
      expect(controller.edgeEvents.containsKey('edge-b'), true);

      // Drop edge-b from mock repository
      fakeStatusRepo.mockEdges = [
        EdgeRegistryView(
          edgeId: 'edge-a',
          edgeName: 'Edge Alpha',
          version: '1.2.0',
          capabilities: ['Serving', 'Automation'],
          protocol: 'iop-wire-v1',
          connected: true,
          lastSeen: DateTime.now(),
        ),
      ];

      // Refresh fleet status
      await controller.fetchEdges();

      // Verify selection falls back to edge-a and edge-b's cache is removed
      expect(controller.selectedEdgeId, 'edge-a');
      expect(controller.edgeStatuses.containsKey('edge-b'), false);
      expect(controller.edgeEvents.containsKey('edge-b'), false);
    },
  );

  test('ignores in-flight fetch completions after dispose', () async {
    final fakeRepo = ControllableStatusRepository();
    final controller = ControlPlaneStatusController(statusRepository: fakeRepo);

    // Start fetching edges
    final fetchFuture = controller.fetchEdges();

    // Verify loading state is true
    expect(controller.loadingEdges, true);

    // Dispose controller
    controller.dispose();

    // Complete the future
    fakeRepo.fleetCompleter.complete(
      FleetStatusResponseView(
        generatedAt: DateTime.now(),
        edges: [
          FleetEdgeView(
            edgeId: 'edge-a',
            edgeName: 'Edge Alpha',
            version: '1.2.0',
            protocol: 'iop-wire-v1',
            connected: true,
            health: 'healthy',
            lastSeen: DateTime.now(),
            nodeCount: 1,
            capabilities: [],
            error: '',
          ),
        ],
      ),
    );

    // Await the future completion
    await fetchFuture;

    // Verify state was NOT updated (should remain default/initial values)
    expect(controller.edges.isEmpty, true);
    expect(
      controller.loadingEdges,
      true,
    ); // Loading edges is still true because mutation didn't happen after dispose
  });

  test('ignores in-flight fetch errors after dispose', () async {
    final fakeRepo = ControllableStatusRepository();
    final controller = ControlPlaneStatusController(statusRepository: fakeRepo);

    final fetchFuture = controller.fetchEdges();

    expect(controller.loadingEdges, true);

    controller.dispose();

    fakeRepo.fleetCompleter.completeError(StateError('fleet failed'));

    await expectLater(fetchFuture, completes);

    expect(controller.edges.isEmpty, true);
    expect(controller.edgesError, isNull);
    expect(controller.loadingEdges, true);
  });

  test('ignores status and events fetch completions after dispose', () async {
    final fakeRepo = ControllableStatusRepository();
    final controller = ControlPlaneStatusController(statusRepository: fakeRepo);

    // Start fetching details
    final detailsFuture = controller.fetchEdgeDetails('edge-a');

    // Verify loading status is set to true
    expect(controller.loadingStatuses['edge-a'], true);
    expect(controller.loadingEvents['edge-a'], true);

    // Dispose controller
    controller.dispose();

    // Complete the status and events
    fakeRepo.statusCompleter.complete(
      EdgeStatusResponseView(
        requestId: 'req-1',
        edgeId: 'edge-a',
        edgeName: 'Edge Alpha',
        observedTimeUnixNano: 0,
        nodes: [],
        capabilities: [],
        metadata: {},
        error: '',
      ),
    );

    fakeRepo.eventsCompleter.complete([]);

    // Await the future completion
    await detailsFuture;

    // Verify state was NOT updated
    expect(controller.edgeStatuses.containsKey('edge-a'), false);
    expect(controller.edgeEvents.containsKey('edge-a'), false);
  });

  test('ignores status and events fetch errors after dispose', () async {
    final fakeRepo = ControllableStatusRepository();
    final controller = ControlPlaneStatusController(statusRepository: fakeRepo);

    final detailsFuture = controller.fetchEdgeDetails('edge-a');

    expect(controller.loadingStatuses['edge-a'], true);
    expect(controller.loadingEvents['edge-a'], true);

    controller.dispose();

    fakeRepo.statusCompleter.completeError(StateError('status failed'));
    fakeRepo.eventsCompleter.completeError(StateError('events failed'));

    await expectLater(detailsFuture, completes);

    expect(controller.edgeStatuses.containsKey('edge-a'), false);
    expect(controller.edgeEvents.containsKey('edge-a'), false);
    expect(controller.statusErrors['edge-a'], isNull);
    expect(controller.eventErrors['edge-a'], isNull);
    expect(controller.loadingStatuses['edge-a'], true);
    expect(controller.loadingEvents['edge-a'], true);
  });

  test('ignores selectEdge after dispose', () {
    final fakeRepo = ControllableStatusRepository();
    final controller = ControlPlaneStatusController(statusRepository: fakeRepo);

    controller.dispose();
    controller.selectEdge('edge-a');

    expect(controller.selectedEdgeId, isNull);
    expect(fakeRepo.statusCallCount, 0);
    expect(fakeRepo.eventsCallCount, 0);
  });
}
