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 _FakeAuthSessionRepository sessions;
  late _FakePasswordHasher passwordHasher;
  late _FakeTokenService tokens;
  late _FakeClock clock;
  late _FakeIds ids;

  setUp(() {
    users = _FakeUserRepository();
    sessions = _FakeAuthSessionRepository();
    passwordHasher = _FakePasswordHasher();
    tokens = _FakeTokenService();
    clock = _FakeClock(DateTime.utc(2026, 7, 19, 12));
    ids = _FakeIds();
  });

  test('register creates a user with a hashed password', () async {
    final useCase = RegisterUserUseCase(
      users: users,
      passwordHasher: passwordHasher,
      clock: clock,
      ids: ids,
    );

    final user = await useCase.execute(
      email: ' USER@Example.COM ',
      password: 'password123',
      displayName: ' Anthony ',
    );

    expect(user.id, 'id-1');
    expect(user.email, 'user@example.com');
    expect(user.passwordHash, 'hashed:password123');
    expect(user.displayName, 'Anthony');
    expect(users.byId[user.id], user);
  });

  test('register rejects an already used email', () async {
    users.add(
      UserAccount(
        id: 'existing-user',
        email: 'user@example.com',
        passwordHash: 'hash',
        createdAt: clock.now(),
        updatedAt: clock.now(),
      ),
    );
    final useCase = RegisterUserUseCase(
      users: users,
      passwordHasher: passwordHasher,
      clock: clock,
      ids: ids,
    );

    await expectLater(
      useCase.execute(email: 'USER@example.com', password: 'password123'),
      throwsA(isA<EmailAlreadyTakenException>()),
    );
  });

  test('login returns a clear token once and stores only its hash', () async {
    users.add(
      UserAccount(
        id: 'user-1',
        email: 'user@example.com',
        passwordHash: 'hashed:password123',
        createdAt: clock.now(),
        updatedAt: clock.now(),
      ),
    );
    tokens.nextToken = 'clear-token';
    final useCase = LoginUseCase(
      users: users,
      sessions: sessions,
      passwordHasher: passwordHasher,
      tokens: tokens,
      clock: clock,
      ids: ids,
    );

    final result = await useCase.execute(
      email: 'user@example.com',
      password: 'password123',
      userAgent: 'test-agent',
      deviceLabel: 'phone',
    );

    expect(result.token, 'clear-token');
    expect(result.expiresAt, DateTime.utc(2026, 8, 18, 12));
    expect(sessions.byTokenHash.keys.single, 'token-hash:clear-token');
    expect(sessions.byTokenHash.values.single.tokenHash, isNot('clear-token'));
  });

  test('login rejects a bad password', () async {
    users.add(
      UserAccount(
        id: 'user-1',
        email: 'user@example.com',
        passwordHash: 'hashed:password123',
        createdAt: clock.now(),
        updatedAt: clock.now(),
      ),
    );
    final useCase = LoginUseCase(
      users: users,
      sessions: sessions,
      passwordHasher: passwordHasher,
      tokens: tokens,
      clock: clock,
      ids: ids,
    );

    await expectLater(
      useCase.execute(email: 'user@example.com', password: 'wrong-password'),
      throwsA(isA<InvalidCredentialsException>()),
    );
  });

  test('authenticate returns the user for a valid token', () async {
    final user = _user(clock);
    users.add(user);
    sessions.add(
      AuthSession(
        id: 'session-1',
        userId: user.id,
        tokenHash: 'token-hash:valid-token',
        issuedAt: clock.now(),
        expiresAt: clock.now().add(const Duration(days: 1)),
      ),
    );
    final useCase = _authenticateUseCase(users, sessions, tokens, clock);

    final authenticated = await useCase.execute(token: 'valid-token');

    expect(authenticated.user.id, user.id);
    expect(authenticated.session.id, 'session-1');
  });

  test('authenticate rejects expired, revoked and unknown tokens', () async {
    final user = _user(clock);
    users.add(user);
    sessions
      ..add(
        AuthSession(
          id: 'expired',
          userId: user.id,
          tokenHash: 'token-hash:expired-token',
          issuedAt: clock.now().subtract(const Duration(days: 2)),
          expiresAt: clock.now().subtract(const Duration(days: 1)),
        ),
      )
      ..add(
        AuthSession(
          id: 'revoked',
          userId: user.id,
          tokenHash: 'token-hash:revoked-token',
          issuedAt: clock.now(),
          expiresAt: clock.now().add(const Duration(days: 1)),
          revokedAt: clock.now(),
        ),
      );
    final useCase = _authenticateUseCase(users, sessions, tokens, clock);

    await expectLater(
      useCase.execute(token: 'expired-token'),
      throwsA(isA<UnauthorizedException>()),
    );
    await expectLater(
      useCase.execute(token: 'revoked-token'),
      throwsA(isA<UnauthorizedException>()),
    );
    await expectLater(
      useCase.execute(token: 'unknown-token'),
      throwsA(isA<UnauthorizedException>()),
    );
  });

  test('logout revokes an active token', () async {
    final user = _user(clock);
    users.add(user);
    sessions.add(
      AuthSession(
        id: 'session-1',
        userId: user.id,
        tokenHash: 'token-hash:valid-token',
        issuedAt: clock.now(),
        expiresAt: clock.now().add(const Duration(days: 1)),
      ),
    );
    final useCase = LogoutUseCase(
      sessions: sessions,
      tokens: tokens,
      clock: clock,
    );

    await useCase.execute(token: 'valid-token');

    expect(sessions.revokedSessionIds, ['session-1']);
  });
}

AuthenticateRequestUseCase _authenticateUseCase(
  _FakeUserRepository users,
  _FakeAuthSessionRepository sessions,
  _FakeTokenService tokens,
  _FakeClock clock,
) {
  return AuthenticateRequestUseCase(
    users: users,
    sessions: sessions,
    tokens: tokens,
    clock: clock,
  );
}

UserAccount _user(_FakeClock clock) {
  return UserAccount(
    id: 'user-1',
    email: 'user@example.com',
    passwordHash: 'hashed:password123',
    createdAt: clock.now(),
    updatedAt: clock.now(),
  );
}

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

  void add(UserAccount user) {
    byId[user.id] = user;
    byEmail[user.email] = user;
  }

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

  @override
  Future<UserAccount?> findById(String id) async => byId[id];

  @override
  Future<void> insert(UserAccount user) async => add(user);

  @override
  Future<void> updatePasswordHash({
    required String userId,
    required String passwordHash,
    required DateTime updatedAt,
  }) async {
    final user = byId[userId];
    if (user == null) {
      return;
    }
    add(
      UserAccount(
        id: user.id,
        email: user.email,
        passwordHash: passwordHash,
        displayName: user.displayName,
        createdAt: user.createdAt,
        updatedAt: updatedAt,
        disabledAt: user.disabledAt,
      ),
    );
  }
}

final class _FakeAuthSessionRepository implements AuthSessionRepository {
  final byTokenHash = <String, AuthSession>{};
  final revokedSessionIds = <String>[];

  void add(AuthSession session) {
    byTokenHash[session.tokenHash] = session;
  }

  @override
  Future<AuthSession?> findByTokenHash(String tokenHash) async {
    return byTokenHash[tokenHash];
  }

  @override
  Future<void> insert(AuthSession session) async => add(session);

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

final class _FakePasswordHasher implements PasswordHasher {
  @override
  Future<String> hash(String password) async => 'hashed:$password';

  @override
  Future<bool> verify({
    required String password,
    required String passwordHash,
  }) async {
    return passwordHash == 'hashed:$password';
  }
}

final class _FakeTokenService implements OpaqueTokenService {
  String nextToken = 'token';

  @override
  String generateToken() => nextToken;

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

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';
  }
}
