import 'dart:convert';

import 'package:gametime_server/api/router.dart';
import 'package:gametime_server/api/share_api.dart';
import 'package:gametime_server/application/application.dart';
import 'package:gametime_server/domain/domain.dart';
import 'package:shelf/shelf.dart';
import 'package:test/test.dart';

void main() {
  test('share routes require bearer authentication', () async {
    final handler = buildApiHandler(shareApi: _shareApi());
    final requests = [
      Request('POST', Uri.parse('http://localhost/shares')),
      Request('GET', Uri.parse('http://localhost/shares/inbox')),
      Request('POST', Uri.parse('http://localhost/shares/share-1/accept')),
      Request('POST', Uri.parse('http://localhost/shares/share-1/decline')),
      Request('POST', Uri.parse('http://localhost/shares/share-1/revoke')),
    ];

    for (final request in requests) {
      final response = await handler(request);

      expect(response.statusCode, 401, reason: request.url.path);
    }
  });

  test(
    'create share returns share id, resolved recipients and unresolved emails',
    () async {
      final shares = _FakeShareRepository();
      final users = _FakeUserRepository([
        _user('user-1', 'user@example.com'),
        _user('user-2', 'friend@example.com'),
      ]);
      final handler = buildApiHandler(
        shareApi: _shareApi(users: users, shares: shares),
      );

      final response = await handler(
        Request(
          'POST',
          Uri.parse('http://localhost/shares'),
          headers: {'authorization': 'Bearer valid-token'},
          body: jsonEncode({
            'resourceType': 'program',
            'payload': {'schemaVersion': 2, 'name': 'Programme été'},
            'recipientEmails': [
              ' friend@example.com ',
              'missing@example.com',
              'user@example.com',
            ],
          }),
        ),
      );

      final body = jsonDecode(await response.readAsString()) as Map;

      expect(response.statusCode, 201);
      expect(body['shareId'], 'id-1');
      expect(body['shareKind'], 'single');
      expect(body['recipientUserIds'], ['user-2']);
      expect(body['unresolvedEmails'], ['missing@example.com']);
      expect(
        shares.shareById['id-1']?.resourceType,
        SyncedResourceType.program,
      );
    },
  );

  test('create pack share returns share id and stores pack metadata', () async {
    final shares = _FakeShareRepository();
    final users = _FakeUserRepository([
      _user('user-1', 'user@example.com'),
      _user('user-2', 'friend@example.com'),
    ]);
    final handler = buildApiHandler(
      shareApi: _shareApi(users: users, shares: shares),
    );

    final response = await handler(
      Request(
        'POST',
        Uri.parse('http://localhost/shares'),
        headers: {'authorization': 'Bearer valid-token'},
        body: jsonEncode({
          'shareKind': 'pack',
          'packName': 'Pack été',
          'items': [
            {
              'resourceType': 'program',
              'payload': {'schemaVersion': 2, 'name': 'Programme'},
            },
            {
              'resourceType': 'workoutTemplate',
              'payload': {'schemaVersion': 3, 'name': 'Séance'},
            },
          ],
          'recipientEmails': ['friend@example.com'],
        }),
      ),
    );

    final body = jsonDecode(await response.readAsString()) as Map;
    final share = shares.shareById['id-1'];

    expect(response.statusCode, 201);
    expect(body['shareKind'], 'pack');
    expect(share?.kind, ShareKind.pack);
    expect(share?.packName, 'Pack été');
    expect(share?.resourceType, isNull);
    expect(share?.payloadJson['items'], hasLength(2));
  });

  test('create share returns 400 for invalid resource type', () async {
    final handler = buildApiHandler(shareApi: _shareApi());

    final response = await handler(
      Request(
        'POST',
        Uri.parse('http://localhost/shares'),
        headers: {'authorization': 'Bearer valid-token'},
        body: jsonEncode({
          'resourceType': 'exercise',
          'payload': {'name': 'Squat'},
          'recipientEmails': ['friend@example.com'],
        }),
      ),
    );

    expect(response.statusCode, 400);
    expect(jsonDecode(await response.readAsString()), {
      'error': 'resourceType must be program or workoutTemplate.',
    });
  });

  test(
    'inbox returns serialized shares for the authenticated recipient',
    () async {
      final shares = _FakeShareRepository()
        ..seedInbox(
          recipientUserId: 'user-1',
          items: [
            ShareInboxItem(
              share: Share(
                id: 'share-1',
                senderUserId: 'sender-1',
                resourceType: SyncedResourceType.program,
                payloadJson: {'name': 'Programme A'},
                createdAt: DateTime.utc(2026, 7, 19, 10),
              ),
              recipient: ShareRecipient(
                id: 'recipient-1',
                shareId: 'share-1',
                recipientUserId: 'user-1',
              ),
            ),
          ],
        );
      final handler = buildApiHandler(shareApi: _shareApi(shares: shares));

      final response = await handler(
        Request(
          'GET',
          Uri.parse('http://localhost/shares/inbox'),
          headers: {'authorization': 'Bearer valid-token'},
        ),
      );

      final body = jsonDecode(await response.readAsString()) as Map;
      final items = body['items'] as List;

      expect(response.statusCode, 200);
      expect(items, hasLength(1));
      expect((items.single as Map)['shareId'], 'share-1');
      expect((items.single as Map)['shareKind'], 'single');
      expect((items.single as Map)['resourceType'], 'program');
      expect((items.single as Map)['status'], 'pending');
    },
  );

  test('accept share returns the copied synced resource', () async {
    final shares = _FakeShareRepository()
      ..seedShare(
        share: Share(
          id: 'share-1',
          senderUserId: 'sender-1',
          resourceType: SyncedResourceType.workoutTemplate,
          payloadJson: {'schemaVersion': 3, 'name': 'Template A'},
          createdAt: DateTime.utc(2026, 7, 19, 10),
        ),
        recipients: [
          ShareRecipient(
            id: 'recipient-1',
            shareId: 'share-1',
            recipientUserId: 'user-1',
          ),
        ],
      );
    final handler = buildApiHandler(shareApi: _shareApi(shares: shares));

    final response = await handler(
      Request(
        'POST',
        Uri.parse('http://localhost/shares/share-1/accept'),
        headers: {'authorization': 'Bearer valid-token'},
      ),
    );

    final body = jsonDecode(await response.readAsString()) as Map;
    final created = body['createdResource'] as Map;

    expect(response.statusCode, 200);
    expect(created['resourceType'], 'workoutTemplate');
    expect(created['payload'], {'schemaVersion': 3, 'name': 'Template A'});
    expect(body['createdResources'], hasLength(1));
    expect(
      shares.recipientById['recipient-1']?.status,
      ShareRecipientStatus.accepted,
    );
    expect(shares.acceptedResources.single.ownerUserId, 'user-1');
  });

  test('accept pack share returns all copied synced resources', () async {
    final shares = _FakeShareRepository()
      ..seedShare(
        share: Share(
          id: 'share-1',
          senderUserId: 'sender-1',
          kind: ShareKind.pack,
          resourceType: null,
          packName: 'Pack été',
          payloadJson: {
            'items': [
              {
                'resourceType': 'program',
                'payload': {'schemaVersion': 2, 'name': 'Programme'},
              },
              {
                'resourceType': 'workoutTemplate',
                'payload': {'schemaVersion': 3, 'name': 'Séance'},
              },
            ],
          },
          createdAt: DateTime.utc(2026, 7, 19, 10),
        ),
        recipients: [
          ShareRecipient(
            id: 'recipient-1',
            shareId: 'share-1',
            recipientUserId: 'user-1',
          ),
        ],
      );
    final handler = buildApiHandler(shareApi: _shareApi(shares: shares));

    final response = await handler(
      Request(
        'POST',
        Uri.parse('http://localhost/shares/share-1/accept'),
        headers: {'authorization': 'Bearer valid-token'},
      ),
    );

    final body = jsonDecode(await response.readAsString()) as Map;
    final created = body['createdResources'] as List;

    expect(response.statusCode, 200);
    expect(body.containsKey('createdResource'), isFalse);
    expect(created.map((item) => (item as Map)['resourceType']), [
      'program',
      'workoutTemplate',
    ]);
    expect(
      shares.recipientById['recipient-1']?.status,
      ShareRecipientStatus.accepted,
    );
  });

  test('accept share returns 404 when share is missing', () async {
    final handler = buildApiHandler(shareApi: _shareApi());

    final response = await handler(
      Request(
        'POST',
        Uri.parse('http://localhost/shares/missing/accept'),
        headers: {'authorization': 'Bearer valid-token'},
      ),
    );

    expect(response.statusCode, 404);
    expect(jsonDecode(await response.readAsString()), {
      'error': 'Share not found.',
    });
  });

  test('accept share returns 409 when share was already answered', () async {
    final shares = _FakeShareRepository()
      ..seedShare(
        share: Share(
          id: 'share-1',
          senderUserId: 'sender-1',
          resourceType: SyncedResourceType.program,
          payloadJson: {'name': 'Programme A'},
          createdAt: DateTime.utc(2026, 7, 19, 10),
        ),
        recipients: [
          ShareRecipient(
            id: 'recipient-1',
            shareId: 'share-1',
            recipientUserId: 'user-1',
            status: ShareRecipientStatus.accepted,
            respondedAt: DateTime.utc(2026, 7, 19, 11),
          ),
        ],
      );
    final handler = buildApiHandler(shareApi: _shareApi(shares: shares));

    final response = await handler(
      Request(
        'POST',
        Uri.parse('http://localhost/shares/share-1/accept'),
        headers: {'authorization': 'Bearer valid-token'},
      ),
    );

    expect(response.statusCode, 409);
    expect(jsonDecode(await response.readAsString()), {
      'error': 'Share has already been answered.',
    });
  });

  test('decline share returns 204 and updates recipient status', () async {
    final shares = _FakeShareRepository()
      ..seedShare(
        share: Share(
          id: 'share-1',
          senderUserId: 'sender-1',
          resourceType: SyncedResourceType.program,
          payloadJson: {'name': 'Programme A'},
          createdAt: DateTime.utc(2026, 7, 19, 10),
        ),
        recipients: [
          ShareRecipient(
            id: 'recipient-1',
            shareId: 'share-1',
            recipientUserId: 'user-1',
          ),
        ],
      );
    final handler = buildApiHandler(shareApi: _shareApi(shares: shares));

    final response = await handler(
      Request(
        'POST',
        Uri.parse('http://localhost/shares/share-1/decline'),
        headers: {'authorization': 'Bearer valid-token'},
      ),
    );

    expect(response.statusCode, 204);
    expect(
      shares.recipientById['recipient-1']?.status,
      ShareRecipientStatus.declined,
    );
  });

  test('decline share returns 409 when share was already answered', () async {
    final shares = _FakeShareRepository()
      ..seedShare(
        share: Share(
          id: 'share-1',
          senderUserId: 'sender-1',
          resourceType: SyncedResourceType.program,
          payloadJson: {'name': 'Programme A'},
          createdAt: DateTime.utc(2026, 7, 19, 10),
        ),
        recipients: [
          ShareRecipient(
            id: 'recipient-1',
            shareId: 'share-1',
            recipientUserId: 'user-1',
            status: ShareRecipientStatus.declined,
            respondedAt: DateTime.utc(2026, 7, 19, 11),
          ),
        ],
      );
    final handler = buildApiHandler(shareApi: _shareApi(shares: shares));

    final response = await handler(
      Request(
        'POST',
        Uri.parse('http://localhost/shares/share-1/decline'),
        headers: {'authorization': 'Bearer valid-token'},
      ),
    );

    expect(response.statusCode, 409);
    expect(jsonDecode(await response.readAsString()), {
      'error': 'Share has already been answered.',
    });
  });

  test('revoke share returns 204 and marks share as revoked', () async {
    final shares = _FakeShareRepository()
      ..seedShare(
        share: Share(
          id: 'share-1',
          senderUserId: 'user-1',
          resourceType: SyncedResourceType.program,
          payloadJson: {'name': 'Programme A'},
          createdAt: DateTime.utc(2026, 7, 19, 10),
        ),
        recipients: const [],
      );
    final handler = buildApiHandler(shareApi: _shareApi(shares: shares));

    final response = await handler(
      Request(
        'POST',
        Uri.parse('http://localhost/shares/share-1/revoke'),
        headers: {'authorization': 'Bearer valid-token'},
      ),
    );

    expect(response.statusCode, 204);
    expect(shares.shareById['share-1']?.isRevoked, isTrue);
  });

  test('revoke share returns 404 when requester is not the sender', () async {
    final shares = _FakeShareRepository()
      ..seedShare(
        share: Share(
          id: 'share-1',
          senderUserId: 'other-user',
          resourceType: SyncedResourceType.program,
          payloadJson: {'name': 'Programme A'},
          createdAt: DateTime.utc(2026, 7, 19, 10),
        ),
        recipients: const [],
      );
    final handler = buildApiHandler(shareApi: _shareApi(shares: shares));

    final response = await handler(
      Request(
        'POST',
        Uri.parse('http://localhost/shares/share-1/revoke'),
        headers: {'authorization': 'Bearer valid-token'},
      ),
    );

    expect(response.statusCode, 404);
    expect(jsonDecode(await response.readAsString()), {
      'error': 'Share not found.',
    });
  });
}

ShareApi _shareApi({_FakeUserRepository? users, _FakeShareRepository? shares}) {
  final clock = _FakeClock(DateTime.utc(2026, 7, 19, 12));
  final ids = _FakeIds();
  final userRepository =
      users ?? _FakeUserRepository([_user('user-1', 'user@example.com')]);
  final sessions = _FakeAuthSessionRepository(
    AuthSession(
      id: 'session-1',
      userId: 'user-1',
      tokenHash: 'token-hash:valid-token',
      issuedAt: clock.now(),
      expiresAt: clock.now().add(const Duration(days: 1)),
    ),
  );
  final tokens = _FakeTokenService();
  final authenticate = AuthenticateRequestUseCase(
    users: userRepository,
    sessions: sessions,
    tokens: tokens,
    clock: clock,
  );
  final shareRepository = shares ?? _FakeShareRepository();
  return ShareApi(
    createShare: CreateShareUseCase(
      users: userRepository,
      shares: shareRepository,
      clock: clock,
      ids: ids,
    ),
    listInbox: ListInboxUseCase(shares: shareRepository),
    acceptShare: AcceptShareUseCase(
      shares: shareRepository,
      clock: clock,
      ids: ids,
    ),
    declineShare: DeclineShareUseCase(shares: shareRepository, clock: clock),
    revokeShare: RevokeShareUseCase(shares: shareRepository, clock: clock),
    authenticateRequest: authenticate,
  );
}

final class _FakeUserRepository implements UserRepository {
  _FakeUserRepository(List<UserAccount> users)
    : _byId = {for (final user in users) user.id: user},
      _byEmail = {for (final user in users) user.email: user};

  final Map<String, UserAccount> _byId;
  final Map<String, UserAccount> _byEmail;

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

  @override
  Future<UserAccount?> findById(String id) async {
    return _byId[id];
  }

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

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

final class _FakeAuthSessionRepository implements AuthSessionRepository {
  const _FakeAuthSessionRepository(this.session);

  final AuthSession session;

  @override
  Future<AuthSession?> findByTokenHash(String tokenHash) async {
    return tokenHash == session.tokenHash ? session : null;
  }

  @override
  Future<void> insert(AuthSession session) async {}

  @override
  Future<void> revoke({
    required String sessionId,
    required DateTime revokedAt,
  }) async {}
}

final class _FakeShareRepository implements ShareRepository {
  final shareById = <String, Share>{};
  final recipientById = <String, ShareRecipient>{};
  final recipientByKey = <String, ShareRecipient>{};
  final inboxByRecipient = <String, List<ShareInboxItem>>{};
  final acceptedResources = <SyncedResource>[];

  void seedShare({
    required Share share,
    required List<ShareRecipient> recipients,
  }) {
    shareById[share.id] = share;
    for (final recipient in recipients) {
      recipientById[recipient.id] = recipient;
      recipientByKey['${recipient.shareId}:${recipient.recipientUserId}'] =
          recipient;
    }
  }

  void seedInbox({
    required String recipientUserId,
    required List<ShareInboxItem> items,
  }) {
    inboxByRecipient[recipientUserId] = items;
    for (final item in items) {
      seedShare(share: item.share, recipients: [item.recipient]);
    }
  }

  @override
  Future<void> insertShare({
    required Share share,
    required List<ShareRecipient> recipients,
  }) async {
    seedShare(share: share, recipients: recipients);
  }

  @override
  Future<List<ShareInboxItem>> listInbox(String recipientUserId) async {
    return inboxByRecipient[recipientUserId] ?? const [];
  }

  @override
  Future<Share?> findShareById(String shareId) async {
    return shareById[shareId];
  }

  @override
  Future<ShareRecipient?> findRecipient({
    required String shareId,
    required String recipientUserId,
  }) async {
    return recipientByKey['$shareId:$recipientUserId'];
  }

  @override
  Future<void> updateRecipientStatus({
    required String recipientId,
    required ShareRecipientStatus status,
    required DateTime respondedAt,
  }) async {
    final existing = recipientById[recipientId];
    if (existing == null) {
      return;
    }
    final updated = ShareRecipient(
      id: existing.id,
      shareId: existing.shareId,
      recipientUserId: existing.recipientUserId,
      status: status,
      respondedAt: respondedAt,
    );
    recipientById[recipientId] = updated;
    recipientByKey['${existing.shareId}:${existing.recipientUserId}'] = updated;

    final inbox = inboxByRecipient[existing.recipientUserId];
    if (inbox != null) {
      inboxByRecipient[existing.recipientUserId] = [
        for (final item in inbox)
          if (item.recipient.id == recipientId)
            ShareInboxItem(share: item.share, recipient: updated)
          else
            item,
      ];
    }
  }

  @override
  Future<List<SyncedResource>> acceptShare({
    required String recipientId,
    required DateTime respondedAt,
    required List<SyncedResource> resources,
  }) async {
    acceptedResources.addAll(resources);
    await updateRecipientStatus(
      recipientId: recipientId,
      status: ShareRecipientStatus.accepted,
      respondedAt: respondedAt,
    );
    return resources;
  }

  @override
  Future<void> revokeShare({
    required String shareId,
    required DateTime revokedAt,
  }) async {
    final existing = shareById[shareId];
    if (existing == null) {
      return;
    }
    final updated = Share(
      id: existing.id,
      senderUserId: existing.senderUserId,
      kind: existing.kind,
      resourceType: existing.resourceType,
      packName: existing.packName,
      payloadJson: existing.payloadJson,
      createdAt: existing.createdAt,
      revokedAt: revokedAt,
    );
    shareById[shareId] = updated;
  }
}

final class _FakeTokenService implements OpaqueTokenService {
  @override
  String generateToken() => 'valid-token';

  @override
  String hashToken(String token) => 'token-hash:$token';
}

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

  final DateTime value;

  @override
  DateTime now() => value;
}

final class _FakeIds implements IdGenerator {
  var _next = 0;

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

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