import 'package:gametime_server/application/application.dart';
import 'package:gametime_server/domain/domain.dart';
import 'package:test/test.dart';

void main() {
  late _FakeUserRepository users;
  late _FakeShareRepository shares;
  late _FakeSyncedResourceRepository resources;
  late _FakeClock clock;
  late _FakeIds ids;

  setUp(() {
    clock = _FakeClock(DateTime.utc(2026, 7, 19, 12));
    ids = _FakeIds();
    users = _FakeUserRepository()
      ..put(_user('sender', 'sender@example.com'))
      ..put(_user('recipient-1', 'friend@example.com'))
      ..put(_user('recipient-2', 'coach@example.com'));
    shares = _FakeShareRepository();
    resources = _FakeSyncedResourceRepository();
  });

  test('create share resolves known and unknown recipient emails', () async {
    final useCase = CreateShareUseCase(
      users: users,
      shares: shares,
      clock: clock,
      ids: ids,
    );

    final result = await useCase.execute(
      senderUserId: 'sender',
      resourceType: 'program',
      payloadJson: {'name': 'Push day'},
      recipientEmails: [
        'friend@example.com',
        'missing@example.com',
        'coach@example.com',
      ],
    );

    expect(result.share.senderUserId, 'sender');
    expect(result.recipientUserIds, ['recipient-1', 'recipient-2']);
    expect(result.unresolvedEmails, ['missing@example.com']);
    expect(shares.recipients.map((recipient) => recipient.recipientUserId), [
      'recipient-1',
      'recipient-2',
    ]);
  });

  test(
    'accept creates a copied synced resource without touching source',
    () async {
      final share = _share(
        id: 'share-1',
        senderUserId: 'sender',
        resourceType: SyncedResourceType.workoutTemplate,
        payloadJson: {'schemaVersion': 3, 'name': 'Full body'},
      );
      shares.putShare(share);
      shares.putRecipient(
        ShareRecipient(
          id: 'recipient-row-1',
          shareId: 'share-1',
          recipientUserId: 'recipient-1',
        ),
      );
      final source = _resource(
        serverId: 'source-server',
        ownerUserId: 'sender',
        resourceType: SyncedResourceType.workoutTemplate,
        clientId: 'source-client',
        payloadJson: {'name': 'Source'},
      );
      resources.put(source);
      final useCase = AcceptShareUseCase(
        shares: shares,
        resources: resources,
        clock: clock,
        ids: ids,
      );

      final result = await useCase.execute(
        shareId: 'share-1',
        recipientUserId: 'recipient-1',
      );

      expect(result.createdResource.ownerUserId, 'recipient-1');
      expect(result.createdResource.clientId, isNot('source-client'));
      expect(result.createdResource.payloadJson, share.payloadJson);
      expect(result.createdResource.schemaVersion, 3);
      expect(resources.getByServerId('source-server'), same(source));
      expect(
        shares.recipient('share-1', 'recipient-1')?.status,
        ShareRecipientStatus.accepted,
      );
    },
  );

  test('accept on already answered share fails', () async {
    shares.putShare(_share(id: 'share-1'));
    shares.putRecipient(
      ShareRecipient(
        id: 'recipient-row-1',
        shareId: 'share-1',
        recipientUserId: 'recipient-1',
        status: ShareRecipientStatus.declined,
        respondedAt: clock.now(),
      ),
    );
    final useCase = AcceptShareUseCase(
      shares: shares,
      resources: resources,
      clock: clock,
      ids: ids,
    );

    expect(
      () => useCase.execute(shareId: 'share-1', recipientUserId: 'recipient-1'),
      throwsA(isA<ShareConflictException>()),
    );
  });

  test('accept on revoked share fails', () async {
    shares.putShare(_share(id: 'share-1', revokedAt: clock.now()));
    shares.putRecipient(
      ShareRecipient(
        id: 'recipient-row-1',
        shareId: 'share-1',
        recipientUserId: 'recipient-1',
      ),
    );
    final useCase = AcceptShareUseCase(
      shares: shares,
      resources: resources,
      clock: clock,
      ids: ids,
    );

    expect(
      () => useCase.execute(shareId: 'share-1', recipientUserId: 'recipient-1'),
      throwsA(isA<ShareConflictException>()),
    );
  });

  test('decline marks a pending recipient as declined', () async {
    shares.putShare(_share(id: 'share-1'));
    shares.putRecipient(
      ShareRecipient(
        id: 'recipient-row-1',
        shareId: 'share-1',
        recipientUserId: 'recipient-1',
      ),
    );
    final useCase = DeclineShareUseCase(shares: shares, clock: clock);

    await useCase.execute(shareId: 'share-1', recipientUserId: 'recipient-1');

    final recipient = shares.recipient('share-1', 'recipient-1');
    expect(recipient?.status, ShareRecipientStatus.declined);
    expect(recipient?.respondedAt, clock.now());
  });

  test('revoke by sender marks the share revoked', () async {
    shares.putShare(_share(id: 'share-1', senderUserId: 'sender'));
    final useCase = RevokeShareUseCase(shares: shares, clock: clock);

    await useCase.execute(shareId: 'share-1', senderUserId: 'sender');

    expect(shares.findShareById('share-1'), completion(isNotNull));
    expect(shares.shares.single.revokedAt, clock.now());
  });

  test('revoke by a non-sender is refused', () async {
    shares.putShare(_share(id: 'share-1', senderUserId: 'sender'));
    final useCase = RevokeShareUseCase(shares: shares, clock: clock);

    expect(
      () => useCase.execute(shareId: 'share-1', senderUserId: 'recipient-1'),
      throwsA(isA<ShareNotFoundException>()),
    );
  });

  test('inbox is sorted from newest to oldest', () async {
    shares.putShare(
      _share(id: 'older', createdAt: DateTime.utc(2026, 7, 19, 10)),
    );
    shares.putShare(
      _share(id: 'newer', createdAt: DateTime.utc(2026, 7, 19, 11)),
    );
    shares
      ..putRecipient(
        ShareRecipient(
          id: 'recipient-row-1',
          shareId: 'older',
          recipientUserId: 'recipient-1',
        ),
      )
      ..putRecipient(
        ShareRecipient(
          id: 'recipient-row-2',
          shareId: 'newer',
          recipientUserId: 'recipient-1',
        ),
      );
    final useCase = ListInboxUseCase(shares: shares);

    final result = await useCase.execute(recipientUserId: 'recipient-1');

    expect(result.map((item) => item.share.id), ['newer', 'older']);
  });
}

UserAccount _user(String id, String email) {
  return UserAccount(
    id: id,
    email: email,
    passwordHash: 'hash',
    createdAt: DateTime.utc(2026, 7, 19),
    updatedAt: DateTime.utc(2026, 7, 19),
  );
}

Share _share({
  required String id,
  String senderUserId = 'sender',
  SyncedResourceType resourceType = SyncedResourceType.program,
  Map<String, Object?> payloadJson = const {'schemaVersion': 1},
  DateTime? createdAt,
  DateTime? revokedAt,
}) {
  return Share(
    id: id,
    senderUserId: senderUserId,
    resourceType: resourceType,
    payloadJson: payloadJson,
    createdAt: createdAt ?? DateTime.utc(2026, 7, 19, 12),
    revokedAt: revokedAt,
  );
}

SyncedResource _resource({
  required String serverId,
  required String ownerUserId,
  required SyncedResourceType resourceType,
  required String clientId,
  required Map<String, Object?> payloadJson,
}) {
  return SyncedResource(
    serverId: serverId,
    ownerUserId: ownerUserId,
    resourceType: resourceType,
    clientId: clientId,
    payloadJson: payloadJson,
    schemaVersion: 1,
    clientUpdatedAt: DateTime.utc(2026, 7, 19, 10),
    serverUpdatedAt: DateTime.utc(2026, 7, 19, 10),
  );
}

final class _FakeUserRepository implements UserRepository {
  final _usersByEmail = <String, UserAccount>{};

  void put(UserAccount user) {
    _usersByEmail[user.email] = user;
  }

  @override
  Future<UserAccount?> findByEmail(String email) async {
    return _usersByEmail[email.toLowerCase()];
  }

  @override
  Future<UserAccount?> findById(String id) async {
    return _usersByEmail.values.where((user) => user.id == id).firstOrNull;
  }

  @override
  Future<void> insert(UserAccount user) async {
    put(user);
  }

  @override
  Future<void> updatePasswordHash({
    required String userId,
    required String passwordHash,
    required DateTime updatedAt,
  }) async {}
}

final class _FakeShareRepository implements ShareRepository {
  final shares = <Share>[];
  final recipients = <ShareRecipient>[];

  void putShare(Share share) {
    shares.removeWhere((item) => item.id == share.id);
    shares.add(share);
  }

  void putRecipient(ShareRecipient recipient) {
    recipients.removeWhere(
      (item) =>
          item.shareId == recipient.shareId &&
          item.recipientUserId == recipient.recipientUserId,
    );
    recipients.add(recipient);
  }

  ShareRecipient? recipient(String shareId, String recipientUserId) {
    return recipients
        .where(
          (item) =>
              item.shareId == shareId &&
              item.recipientUserId == recipientUserId,
        )
        .firstOrNull;
  }

  @override
  Future<void> insertShare({
    required Share share,
    required List<ShareRecipient> recipients,
  }) async {
    putShare(share);
    for (final recipient in recipients) {
      putRecipient(recipient);
    }
  }

  @override
  Future<List<ShareInboxItem>> listInbox(String recipientUserId) async {
    final items =
        [
          for (final recipient in recipients)
            if (recipient.recipientUserId == recipientUserId)
              ShareInboxItem(
                share: shares.singleWhere(
                  (share) => share.id == recipient.shareId,
                ),
                recipient: recipient,
              ),
        ]..sort(
          (left, right) =>
              right.share.createdAt.compareTo(left.share.createdAt),
        );
    return items;
  }

  @override
  Future<Share?> findShareById(String shareId) async {
    return shares.where((share) => share.id == shareId).firstOrNull;
  }

  @override
  Future<ShareRecipient?> findRecipient({
    required String shareId,
    required String recipientUserId,
  }) async {
    return recipient(shareId, recipientUserId);
  }

  @override
  Future<void> updateRecipientStatus({
    required String recipientId,
    required ShareRecipientStatus status,
    required DateTime respondedAt,
  }) async {
    final index = recipients.indexWhere((item) => item.id == recipientId);
    final recipient = recipients[index];
    recipients[index] = ShareRecipient(
      id: recipient.id,
      shareId: recipient.shareId,
      recipientUserId: recipient.recipientUserId,
      status: status,
      respondedAt: respondedAt,
    );
  }

  @override
  Future<void> revokeShare({
    required String shareId,
    required DateTime revokedAt,
  }) async {
    final index = shares.indexWhere((share) => share.id == shareId);
    final share = shares[index];
    shares[index] = Share(
      id: share.id,
      senderUserId: share.senderUserId,
      resourceType: share.resourceType,
      payloadJson: share.payloadJson,
      createdAt: share.createdAt,
      revokedAt: revokedAt,
    );
  }
}

final class _FakeSyncedResourceRepository implements SyncedResourceRepository {
  final _items = <String, SyncedResource>{};

  void put(SyncedResource resource) {
    _items[_key(
          resource.ownerUserId,
          resource.resourceType,
          resource.clientId,
        )] =
        resource;
  }

  SyncedResource? getByServerId(String serverId) {
    return _items.values.where((item) => item.serverId == serverId).firstOrNull;
  }

  @override
  Future<SyncWriteResult> upsertWithLww(SyncedResource resource) async {
    final key = _key(
      resource.ownerUserId,
      resource.resourceType,
      resource.clientId,
    );
    final existing = _items[key];
    if (existing != null &&
        !resource.clientUpdatedAt.isAfter(existing.clientUpdatedAt)) {
      return SyncWriteResult(
        status: SyncWriteStatus.ignoredOlder,
        resource: existing,
      );
    }
    _items[key] = resource;
    return SyncWriteResult(
      status: SyncWriteStatus.accepted,
      resource: resource,
    );
  }

  @override
  Future<List<SyncedResource>> findAllForUserSince({
    required String ownerUserId,
    DateTime? since,
  }) async {
    return _items.values
        .where((item) => item.ownerUserId == ownerUserId)
        .where((item) => since == null || item.serverUpdatedAt.isAfter(since))
        .toList();
  }
}

String _key(
  String ownerUserId,
  SyncedResourceType resourceType,
  String clientId,
) {
  return '$ownerUserId:${resourceType.wireName}:$clientId';
}

final class _FakeClock implements Clock {
  _FakeClock(this.value);

  DateTime value;

  @override
  DateTime now() => value;
}

final class _FakeIds implements IdGenerator {
  var _next = 0;

  @override
  String newId() {
    _next += 1;
    return 'id-$_next';
  }
}
