import 'package:flutter_test/flutter_test.dart';

import 'package:auth/auth.dart';

void main() {
  // ---------------------------------------------------------------------------
  // KcUser
  // ---------------------------------------------------------------------------
  group('KcUser', () {
    const user = KcUser(
      id: 'u1',
      displayName: 'Alice',
      email: 'alice@example.com',
      role: KcUserRole.operator,
    );

    test('stores all fields', () {
      expect(user.id, 'u1');
      expect(user.displayName, 'Alice');
      expect(user.email, 'alice@example.com');
      expect(user.role, KcUserRole.operator);
    });

    test('copyWith replaces specified fields', () {
      final updated = user.copyWith(displayName: 'Bob', role: KcUserRole.admin);
      expect(updated.id, 'u1');
      expect(updated.displayName, 'Bob');
      expect(updated.email, 'alice@example.com');
      expect(updated.role, KcUserRole.admin);
    });

    test('copyWith with no args returns equivalent user', () {
      final copy = user.copyWith();
      expect(copy, equals(user));
    });

    test('equality compares all fields', () {
      const same = KcUser(
        id: 'u1',
        displayName: 'Alice',
        email: 'alice@example.com',
        role: KcUserRole.operator,
      );
      const different = KcUser(
        id: 'u2',
        displayName: 'Alice',
        email: 'alice@example.com',
        role: KcUserRole.operator,
      );
      expect(user, equals(same));
      expect(user, isNot(equals(different)));
      expect(user.hashCode, equals(same.hashCode));
    });

    test('toString includes key fields', () {
      expect(user.toString(), contains('u1'));
      expect(user.toString(), contains('Alice'));
      expect(user.toString(), contains('operator'));
    });
  });

  // ---------------------------------------------------------------------------
  // KcUserRole
  // ---------------------------------------------------------------------------
  group('KcUserRole', () {
    test('has three values', () {
      expect(KcUserRole.values.length, 3);
    });

    test('values are admin, operator, viewer', () {
      expect(
        KcUserRole.values,
        containsAll([KcUserRole.admin, KcUserRole.operator, KcUserRole.viewer]),
      );
    });
  });

  // ---------------------------------------------------------------------------
  // AuthState
  // ---------------------------------------------------------------------------
  group('AuthState', () {
    const user = KcUser(
      id: 'u1',
      displayName: 'Alice',
      email: 'alice@example.com',
      role: KcUserRole.admin,
    );

    test('unauthenticated: isAuthenticated is false', () {
      const state = AuthStateUnauthenticated();
      expect(state.isAuthenticated, isFalse);
      expect(state.isLoading, isFalse);
      expect(state.user, isNull);
    });

    test('loading: isLoading is true', () {
      const state = AuthStateLoading();
      expect(state.isLoading, isTrue);
      expect(state.isAuthenticated, isFalse);
      expect(state.user, isNull);
    });

    test('authenticated: isAuthenticated is true and user is set', () {
      final state = AuthStateAuthenticated(user: user);
      expect(state.isAuthenticated, isTrue);
      expect(state.isLoading, isFalse);
      expect(state.user, equals(user));
    });

    test('authenticated: equality compares user', () {
      final a = AuthStateAuthenticated(user: user);
      final b = AuthStateAuthenticated(user: user);
      const c = KcUser(
        id: 'u2',
        displayName: 'Bob',
        email: 'bob@example.com',
        role: KcUserRole.viewer,
      );
      final d = AuthStateAuthenticated(user: c);
      expect(a, equals(b));
      expect(a, isNot(equals(d)));
      expect(a.hashCode, equals(b.hashCode));
    });

    test('error: message is stored', () {
      const state = AuthStateError(message: 'Invalid credentials');
      expect(state.isAuthenticated, isFalse);
      expect(state.user, isNull);
      expect(state.message, 'Invalid credentials');
    });

    test('error: equality compares message', () {
      const a = AuthStateError(message: 'oops');
      const b = AuthStateError(message: 'oops');
      const c = AuthStateError(message: 'different');
      expect(a, equals(b));
      expect(a, isNot(equals(c)));
    });

    test('toString includes state type', () {
      expect(const AuthStateUnauthenticated().toString(), contains('unauthenticated'));
      expect(const AuthStateLoading().toString(), contains('loading'));
      expect(AuthStateAuthenticated(user: user).toString(), contains('authenticated'));
      expect(const AuthStateError(message: 'x').toString(), contains('error'));
    });
  });

  // ---------------------------------------------------------------------------
  // AuthException
  // ---------------------------------------------------------------------------
  group('AuthException', () {
    test('stores message and optional statusCode', () {
      const ex = AuthException(message: 'Unauthorized', statusCode: 401);
      expect(ex.message, 'Unauthorized');
      expect(ex.statusCode, 401);
    });

    test('toString includes message', () {
      const ex = AuthException(message: 'Forbidden');
      expect(ex.toString(), contains('Forbidden'));
    });

    test('toString includes HTTP status when present', () {
      const ex = AuthException(message: 'Unauthorized', statusCode: 401);
      expect(ex.toString(), contains('401'));
    });

    test('statusCode is optional', () {
      const ex = AuthException(message: 'Network error');
      expect(ex.statusCode, isNull);
    });
  });

  // ---------------------------------------------------------------------------
  // CredentialStore / CredentialKeys
  // ---------------------------------------------------------------------------
  group('InMemoryCredentialStore', () {
    late InMemoryCredentialStore store;

    setUp(() => store = InMemoryCredentialStore());

    test('read returns null for missing key', () async {
      expect(await store.read('missing'), isNull);
    });

    test('write and read round-trip', () async {
      await store.write(CredentialKeys.wcConsumerKey, 'ck_test');
      expect(await store.read(CredentialKeys.wcConsumerKey), 'ck_test');
    });

    test('write overwrites existing value', () async {
      await store.write('key', 'first');
      await store.write('key', 'second');
      expect(await store.read('key'), 'second');
    });

    test('delete removes key', () async {
      await store.write('key', 'value');
      await store.delete('key');
      expect(await store.read('key'), isNull);
    });

    test('delete is no-op for missing key', () async {
      await expectLater(store.delete('missing'), completes);
    });

    test('deleteAll clears all keys', () async {
      await store.write('a', '1');
      await store.write('b', '2');
      await store.deleteAll();
      expect(await store.read('a'), isNull);
      expect(await store.read('b'), isNull);
    });

    test('containsKey returns true for present key', () async {
      await store.write('key', 'value');
      expect(await store.containsKey('key'), isTrue);
    });

    test('containsKey returns false for missing key', () async {
      expect(await store.containsKey('missing'), isFalse);
    });
  });

  group('CredentialKeys', () {
    test('all keys are non-empty strings', () {
      expect(CredentialKeys.wcConsumerKey, isNotEmpty);
      expect(CredentialKeys.wcConsumerSecret, isNotEmpty);
      expect(CredentialKeys.wcSiteUrl, isNotEmpty);
      expect(CredentialKeys.squareAccessToken, isNotEmpty);
      expect(CredentialKeys.squareLocationId, isNotEmpty);
      expect(CredentialKeys.sessionToken, isNotEmpty);
    });

    test('all keys are unique', () {
      final keys = [
        CredentialKeys.wcConsumerKey,
        CredentialKeys.wcConsumerSecret,
        CredentialKeys.wcSiteUrl,
        CredentialKeys.squareAccessToken,
        CredentialKeys.squareLocationId,
        CredentialKeys.sessionToken,
      ];
      expect(keys.toSet().length, keys.length);
    });
  });

  // ---------------------------------------------------------------------------
  // FakeAuthService
  // ---------------------------------------------------------------------------
  group('FakeAuthService', () {
    late FakeAuthService service;

    setUp(() => service = FakeAuthService());
    tearDown(() => service.dispose());

    test('initial state is unauthenticated', () {
      expect(service.currentState, isA<AuthStateUnauthenticated>());
      expect(service.currentUser, isNull);
    });

    test('authStateStream emits current state immediately', () async {
      final first = await service.authStateStream.first;
      expect(first, isA<AuthStateUnauthenticated>());
    });

    test('signIn with valid credentials returns user', () async {
      final user = await service.signIn(email: 'operator@kellcreations.com', password: 'secret');
      expect(user.email, 'operator@kellcreations.com');
      expect(user.role, KcUserRole.operator);
    });

    test('signIn sets state to authenticated', () async {
      await service.signIn(email: 'admin@kellcreations.com', password: 'pw');
      expect(service.currentState, isA<AuthStateAuthenticated>());
      expect(service.currentUser?.role, KcUserRole.admin);
    });

    test('signIn with admin email assigns admin role', () async {
      final user = await service.signIn(email: 'admin@kellcreations.com', password: 'pw');
      expect(user.role, KcUserRole.admin);
    });

    test('signIn with operator email assigns operator role', () async {
      final user = await service.signIn(email: 'operator@kellcreations.com', password: 'pw');
      expect(user.role, KcUserRole.operator);
    });

    test('signIn with unknown email assigns viewer role', () async {
      final user = await service.signIn(email: 'someone@example.com', password: 'pw');
      expect(user.role, KcUserRole.viewer);
    });

    test('signIn with empty email throws AuthException', () async {
      await expectLater(service.signIn(email: '', password: 'pw'), throwsA(isA<AuthException>()));
      expect(service.currentState, isA<AuthStateError>());
    });

    test('signIn with empty password throws AuthException', () async {
      await expectLater(
        service.signIn(email: 'user@example.com', password: ''),
        throwsA(isA<AuthException>()),
      );
      expect(service.currentState, isA<AuthStateError>());
    });

    test('signOut sets state to unauthenticated', () async {
      await service.signIn(email: 'admin@kellcreations.com', password: 'pw');
      await service.signOut();
      expect(service.currentState, isA<AuthStateUnauthenticated>());
      expect(service.currentUser, isNull);
    });

    test('refreshSession returns true when authenticated', () async {
      await service.signIn(email: 'admin@kellcreations.com', password: 'pw');
      expect(await service.refreshSession(), isTrue);
    });

    test('refreshSession returns false when unauthenticated', () async {
      expect(await service.refreshSession(), isFalse);
    });

    test('authStateStream emits loading then authenticated on signIn', () async {
      final states = <AuthState>[];
      final sub = service.authStateStream.listen(states.add);

      await service.signIn(email: 'admin@kellcreations.com', password: 'pw');
      // Allow any pending microtasks to flush.
      await Future<void>.delayed(Duration.zero);
      await sub.cancel();

      // Stream: unauthenticated (initial), loading, authenticated
      expect(states.length, greaterThanOrEqualTo(2));
      expect(states.any((s) => s is AuthStateLoading), isTrue);
      expect(states.last, isA<AuthStateAuthenticated>());
    });

    test('authStateStream emits loading then unauthenticated on signOut', () async {
      await service.signIn(email: 'admin@kellcreations.com', password: 'pw');

      final states = <AuthState>[];
      final sub = service.authStateStream.listen(states.add);

      await service.signOut();
      // Allow any pending microtasks to flush.
      await Future<void>.delayed(Duration.zero);
      await sub.cancel();

      expect(states.last, isA<AuthStateUnauthenticated>());
    });
  });
}
