import 'package:flutter_test/flutter_test.dart';
import 'package:smplayer/core/contracts/auth.dart';
import 'package:smplayer/core/contracts/cancellation.dart';
import 'package:smplayer/core/contracts/library.dart';
import 'package:smplayer/core/contracts/server.dart';
import 'package:smplayer/core/domain/ports/emby_gateway.dart';
import 'package:smplayer/core/domain/ports/server_repository.dart';
import 'package:smplayer/core/domain/ports/session_repository.dart';
import 'package:smplayer/core/domain/usecases/media_usecases.dart';

class _FakeSessionRepository implements SessionRepository {
  @override
  Future<String?> getActiveServerId() async => 'active';

  @override
  Future<List<SessionData>> listAll() async => [
    _buildSession('active'),
    _buildSession('remote'),
  ];

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

class _FakeServerRepository implements ServerRepository {
  @override
  Future<List<EmbyServer>> list() async => [
    _buildServer('active'),
    _buildServer('remote'),
  ];

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

class _ProviderFilteringEmbyGateway implements EmbyGateway {
  _ProviderFilteringEmbyGateway({
    this.providerMovieItems = const [],
    this.nameSearchMovieItems = const [],
    this.seriesItems = const [],
  });

  final List<EmbyRawItem> providerMovieItems;
  final List<EmbyRawItem> nameSearchMovieItems;
  final List<EmbyRawItem> seriesItems;

  int providerMovieQueryCount = 0;
  int nameSearchMovieQueryCount = 0;
  int seriesQueryCount = 0;
  int detailQueryCount = 0;
  int seasonsQueryCount = 0;

  Iterable<EmbyRawItem> get _allItems => [
    ...providerMovieItems,
    ...nameSearchMovieItems,
    ...seriesItems,
  ];

  @override
  Future<List<EmbyRawItem>> getLibraryItems({
    required AuthedRequestContext ctx,
    String? parentId,
    String? includeItemTypes,
    List<String>? genreIds,
    String? searchTerm,
    String? anyProviderIdEquals,
    List<String>? personIds,
    String? fields,
    String? enableImageTypes,
    bool? groupProgramsBySeries,
    int? imageTypeLimit,
    int? startIndex,
    int? limit,
    bool? recursive,
    String? sortBy,
    SortOrder? sortOrder,
    String? filters,
    CancellationToken? cancelToken,
  }) async {
    if (includeItemTypes == 'Movie' && anyProviderIdEquals != null) {
      providerMovieQueryCount++;
      return providerMovieItems;
    }
    if (includeItemTypes == 'Movie' && searchTerm != null) {
      nameSearchMovieQueryCount++;
      return nameSearchMovieItems;
    }
    if (includeItemTypes == 'Series') {
      seriesQueryCount++;
      return seriesItems;
    }
    return const [];
  }

  @override
  Future<EmbyRawItem> getItemDetail(
    AuthedRequestContext ctx,
    String itemId,
  ) async {
    detailQueryCount++;
    return _allItems.firstWhere((item) => item.Id == itemId);
  }

  @override
  Future<List<EmbyRawSeason>> getSeriesSeasons(
    AuthedRequestContext ctx,
    String seriesId, {
    CancellationToken? cancelToken,
  }) async {
    seasonsQueryCount++;
    return const [];
  }

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

EmbyServer _buildServer(String serverId) => EmbyServer(
  id: serverId,
  name: serverId,
  baseUrl: 'http://$serverId',
  createdAt: '2026-01-01T00:00:00Z',
  updatedAt: '2026-01-01T00:00:00Z',
);

SessionData _buildSession(String serverId) => SessionData(
  serverId: serverId,
  token: 'token-$serverId',
  userId: 'user-$serverId',
  userName: serverId,
  createdAt: '2026-01-01T00:00:00Z',
);

EmbyRawItem _buildMovie({
  required String itemId,
  required String mediaSourceId,
  Map<String, String>? providerIds,
}) => EmbyRawItem.fromJson({
  'Id': itemId,
  'Name': 'Movie One',
  'Type': 'Movie',
  if (providerIds != null) 'ProviderIds': providerIds,
  'MediaSources': [
    {'Id': mediaSourceId, 'Name': '4K'},
  ],
});

CrossServerSearchUseCase _buildUseCase(
  _ProviderFilteringEmbyGateway embyGateway,
) => CrossServerSearchUseCase(
  sessionRepository: _FakeSessionRepository(),
  serverRepository: _FakeServerRepository(),
  embyGateway: embyGateway,
);

Future<List<CrossServerSourceCard>> _collectCards(
  CrossServerSearchUseCase useCase,
  CrossServerSearchReq request,
) async {
  final emittedCardLists = await useCase.executeStream(request).toList();
  return emittedCardLists.isEmpty ? const [] : emittedCardLists.last;
}

void main() {
  const movieRequest = CrossServerSearchReq(
    anchorType: 'Movie',
    itemName: 'Movie One',
    providerIds: {'Tmdb': '42'},
  );

  group('CrossServerSearchUseCase provider-id filtering', () {
    test('drops ignored provider-query results before detail fetch', () async {
      final mismatchedMovie = _buildMovie(
        itemId: 'unrelated-movie',
        mediaSourceId: 'unrelated-source',
        providerIds: const {'Tmdb': '999'},
      );
      final embyGateway = _ProviderFilteringEmbyGateway(
        providerMovieItems: [mismatchedMovie],
        nameSearchMovieItems: [mismatchedMovie],
      );

      final cards = await _collectCards(
        _buildUseCase(embyGateway),
        movieRequest,
      );

      expect(cards, isEmpty);
      expect(embyGateway.providerMovieQueryCount, 1);
      expect(embyGateway.nameSearchMovieQueryCount, 1);
      expect(embyGateway.detailQueryCount, 0);
    });

    test(
      'keeps provider-query results with a normalized matching id',
      () async {
        final matchingMovie = _buildMovie(
          itemId: 'matching-movie',
          mediaSourceId: 'matching-source',
          providerIds: const {'tmdb': '42'},
        );
        final embyGateway = _ProviderFilteringEmbyGateway(
          providerMovieItems: [matchingMovie],
        );

        final cards = await _collectCards(
          _buildUseCase(embyGateway),
          movieRequest,
        );

        expect(cards, hasLength(1));
        expect(cards.single.mediaSourceId, 'matching-source');
        expect(embyGateway.nameSearchMovieQueryCount, 0);
        expect(embyGateway.detailQueryCount, 1);
      },
    );

    test(
      'drops conflicting name results but keeps unscanned results',
      () async {
        final conflictingMovie = _buildMovie(
          itemId: 'conflicting-movie',
          mediaSourceId: 'conflicting-source',
          providerIds: const {'Tmdb': '999'},
        );
        final unscannedMovie = _buildMovie(
          itemId: 'unscanned-movie',
          mediaSourceId: 'unscanned-source',
        );
        final embyGateway = _ProviderFilteringEmbyGateway(
          nameSearchMovieItems: [conflictingMovie, unscannedMovie],
        );

        final cards = await _collectCards(
          _buildUseCase(embyGateway),
          movieRequest,
        );

        expect(cards, hasLength(1));
        expect(cards.single.landingItemId, 'unscanned-movie');
        expect(cards.single.mediaSourceId, 'unscanned-source');
        expect(embyGateway.detailQueryCount, 1);
      },
    );

    test('rejects and caches a mismatched remote series identity', () async {
      final mismatchedSeries = EmbyRawItem.fromJson(const {
        'Id': 'unrelated-series',
        'Name': 'Unrelated Series',
        'Type': 'Series',
        'ProviderIds': {'Tmdb': '999'},
      });
      final embyGateway = _ProviderFilteringEmbyGateway(
        seriesItems: [mismatchedSeries],
      );
      final useCase = _buildUseCase(embyGateway);
      const episodeRequest = CrossServerSearchReq(
        anchorType: 'Episode',
        itemName: 'Episode One',
        providerIds: {},
        seriesProviderIds: {'Tmdb': '42'},
        parentIndexNumber: 1,
        indexNumber: 1,
      );

      final firstCards = await _collectCards(useCase, episodeRequest);
      final secondCards = await _collectCards(useCase, episodeRequest);

      expect(firstCards, isEmpty);
      expect(secondCards, isEmpty);
      expect(embyGateway.seriesQueryCount, 1);
      expect(embyGateway.seasonsQueryCount, 0);
    });
  });
}
