import 'package:flutter_test/flutter_test.dart';
import 'package:data/data.dart';

// ── Test doubles ─────────────────────────────────────────────────────────────

/// Simple domain type used across all tests.
class _Item {
  final String id;
  final String name;
  const _Item(this.id, this.name);
  @override
  String toString() => '_Item($id, $name)';
}

/// Minimal [DataMapper] implementation for tests.
class _ItemMapper extends DataMapper<_Item, Map<String, dynamic>> {
  @override
  _Item fromRemote(Map<String, dynamic> remote) {
    final id = remote['id'];
    final name = remote['name'];
    if (id == null || name == null) {
      throw DataMappingException(field: 'id/name', message: 'Required fields missing');
    }
    return _Item(id as String, name as String);
  }

  @override
  Map<String, dynamic> toRemote(_Item local) => {'id': local.id, 'name': local.name};
}

/// A [ConflictResolver] that always merges by concatenating names.
class _MergeResolver implements ConflictResolver<_Item> {
  const _MergeResolver();

  @override
  ConflictResult<_Item> resolve({
    required _Item local,
    required _Item remote,
    required DateTime localModifiedAt,
    required DateTime remoteModifiedAt,
  }) {
    return ConflictResult.merged(
      merged: _Item(local.id, '${local.name}+${remote.name}'),
      reason: 'merged names',
    );
  }
}

void main() {
  // ── LocalCache / InMemoryCache ────────────────────────────────────────────

  group('InMemoryCache', () {
    late InMemoryCache<String> cache;

    setUp(() => cache = InMemoryCache<String>());
    tearDown(() async => cache.dispose());

    test('put and get returns stored value', () async {
      await cache.put('k', 'hello');
      expect(await cache.get('k'), 'hello');
    });

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

    test('containsKey returns true for present key', () async {
      await cache.put('k', 'v');
      expect(await cache.containsKey('k'), isTrue);
    });

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

    test('remove deletes the entry', () async {
      await cache.put('k', 'v');
      await cache.remove('k');
      expect(await cache.get('k'), isNull);
    });

    test('clear removes all entries', () async {
      await cache.put('a', '1');
      await cache.put('b', '2');
      await cache.clear();
      expect(await cache.keys(), isEmpty);
    });

    test('keys returns all non-expired keys', () async {
      await cache.put('x', '1');
      await cache.put('y', '2');
      final keys = await cache.keys();
      expect(keys, containsAll(['x', 'y']));
    });

    test('entry expires after ttl', () async {
      await cache.put('k', 'v', ttl: const Duration(milliseconds: 1));
      await Future<void>.delayed(const Duration(milliseconds: 10));
      expect(await cache.get('k'), isNull);
    });

    test('containsKey returns false for expired entry', () async {
      await cache.put('k', 'v', ttl: const Duration(milliseconds: 1));
      await Future<void>.delayed(const Duration(milliseconds: 10));
      expect(await cache.containsKey('k'), isFalse);
    });

    test('entry without ttl does not expire', () async {
      await cache.put('k', 'persistent');
      await Future<void>.delayed(const Duration(milliseconds: 10));
      expect(await cache.get('k'), 'persistent');
    });

    test('dispose clears all entries', () async {
      await cache.put('k', 'v');
      await cache.dispose();
      expect(await cache.keys(), isEmpty);
    });

    test('overwriting a key replaces the value', () async {
      await cache.put('k', 'first');
      await cache.put('k', 'second');
      expect(await cache.get('k'), 'second');
    });
  });

  // ── SyncQueue / InMemorySyncQueue ─────────────────────────────────────────

  group('SyncOperation', () {
    final op = SyncOperation(
      id: 'op-1',
      type: 'createOrder',
      payload: {'orderId': 42},
      enqueuedAt: DateTime(2026, 7, 11),
    );

    test('stores all fields with defaults', () {
      expect(op.id, 'op-1');
      expect(op.type, 'createOrder');
      expect(op.payload['orderId'], 42);
      expect(op.status, SyncOperationStatus.pending);
      expect(op.attemptCount, 0);
      expect(op.lastError, isNull);
    });

    test('copyWith replaces specified fields', () {
      final updated = op.copyWith(
        status: SyncOperationStatus.failed,
        attemptCount: 2,
        lastError: 'timeout',
      );
      expect(updated.status, SyncOperationStatus.failed);
      expect(updated.attemptCount, 2);
      expect(updated.lastError, 'timeout');
      expect(updated.id, op.id);
    });

    test('toString includes id, type, status, attempts', () {
      expect(op.toString(), contains('op-1'));
      expect(op.toString(), contains('createOrder'));
    });
  });

  group('SyncFlushResult', () {
    final op = SyncOperation(
      id: 'op-1',
      type: 'adjustInventory',
      payload: {},
      enqueuedAt: DateTime(2026, 7, 11),
    );

    test('success constructor sets success true and no error', () {
      final result = SyncFlushResult.success(operation: op);
      expect(result.success, isTrue);
      expect(result.errorMessage, isNull);
    });

    test('failure constructor sets success false and error message', () {
      final result = SyncFlushResult.failure(operation: op, errorMessage: 'network error');
      expect(result.success, isFalse);
      expect(result.errorMessage, 'network error');
    });

    test('toString includes operation id and success flag', () {
      final result = SyncFlushResult.success(operation: op);
      expect(result.toString(), contains('op-1'));
      expect(result.toString(), contains('true'));
    });
  });

  group('InMemorySyncQueue', () {
    late InMemorySyncQueue queue;

    SyncOperation op0(String id) =>
        SyncOperation(id: id, type: 'test', payload: {}, enqueuedAt: DateTime(2026, 7, 11));

    setUp(() => queue = InMemorySyncQueue());

    test('enqueue adds operation and length increases', () async {
      await queue.enqueue(op0('a'));
      expect(await queue.length, 1);
    });

    test('pendingOperations returns only pending ops', () async {
      await queue.enqueue(op0('a'));
      await queue.enqueue(op0('b'));
      final pending = await queue.pendingOperations();
      expect(pending.length, 2);
      expect(pending.every((op) => op.status == SyncOperationStatus.pending), isTrue);
    });

    test('process succeeds and marks operation completed', () async {
      final op = op0('a');
      await queue.enqueue(op);
      final result = await queue.process(op, (_) async {});
      expect(result.success, isTrue);
      expect(result.operation.status, SyncOperationStatus.completed);
      expect(result.operation.attemptCount, 1);
    });

    test('process failure marks operation failed with error', () async {
      final op = op0('a');
      await queue.enqueue(op);
      final result = await queue.process(op, (_) async => throw Exception('boom'));
      expect(result.success, isFalse);
      expect(result.operation.status, SyncOperationStatus.failed);
      expect(result.errorMessage, contains('boom'));
      expect(result.operation.attemptCount, 1);
    });

    test('process returns failure for unknown operation id', () async {
      final op = op0('unknown');
      final result = await queue.process(op, (_) async {});
      expect(result.success, isFalse);
      expect(result.errorMessage, contains('not found'));
    });

    test('remove deletes operation from queue', () async {
      await queue.enqueue(op0('a'));
      await queue.remove('a');
      expect(await queue.length, 0);
    });

    test('pruneCompleted removes completed and failed ops', () async {
      final opA = op0('a');
      final opB = op0('b');
      final opC = op0('c');
      await queue.enqueue(opA);
      await queue.enqueue(opB);
      await queue.enqueue(opC);
      await queue.process(opA, (_) async {}); // completed
      await queue.process(opB, (_) async => throw Exception('err')); // failed
      // opC remains pending
      await queue.pruneCompleted();
      expect(await queue.length, 1);
      final pending = await queue.pendingOperations();
      expect(pending.first.id, 'c');
    });

    test('pendingOperations excludes completed operations', () async {
      final op = op0('a');
      await queue.enqueue(op);
      await queue.process(op, (_) async {});
      final pending = await queue.pendingOperations();
      expect(pending, isEmpty);
    });
  });

  // ── ConflictResolver ──────────────────────────────────────────────────────

  group('ConflictResult', () {
    const item = _Item('1', 'Widget');

    test('useLocal constructor sets resolution and value', () {
      final result = ConflictResult.useLocal(local: item, reason: 'test');
      expect(result.resolution, ConflictResolution.useLocal);
      expect(result.resolvedValue, item);
      expect(result.reason, 'test');
    });

    test('useRemote constructor sets resolution and value', () {
      final result = ConflictResult.useRemote(remote: item, reason: 'test');
      expect(result.resolution, ConflictResolution.useRemote);
      expect(result.resolvedValue, item);
    });

    test('merged constructor sets resolution and value', () {
      final result = ConflictResult.merged(merged: item, reason: 'merged');
      expect(result.resolution, ConflictResolution.merge);
      expect(result.resolvedValue, item);
    });

    test('requiresReview constructor sets resolution and null value', () {
      const result = ConflictResult<_Item>.requiresReview(reason: 'conflict');
      expect(result.resolution, ConflictResolution.requiresOperatorReview);
      expect(result.resolvedValue, isNull);
    });

    test('toString includes resolution and reason', () {
      final result = ConflictResult.useLocal(local: item, reason: 'newer');
      expect(result.toString(), contains('useLocal'));
      expect(result.toString(), contains('newer'));
    });
  });

  group('LastWriteWinsResolver', () {
    const resolver = LastWriteWinsResolver<_Item>();
    const local = _Item('1', 'Local');
    const remote = _Item('1', 'Remote');
    final older = DateTime(2026, 1, 1);
    final newer = DateTime(2026, 7, 11);

    test('prefers local when local is newer', () {
      final result = resolver.resolve(
        local: local,
        remote: remote,
        localModifiedAt: newer,
        remoteModifiedAt: older,
      );
      expect(result.resolution, ConflictResolution.useLocal);
      expect(result.resolvedValue, local);
    });

    test('prefers remote when remote is newer', () {
      final result = resolver.resolve(
        local: local,
        remote: remote,
        localModifiedAt: older,
        remoteModifiedAt: newer,
      );
      expect(result.resolution, ConflictResolution.useRemote);
      expect(result.resolvedValue, remote);
    });

    test('prefers remote when timestamps are equal', () {
      final result = resolver.resolve(
        local: local,
        remote: remote,
        localModifiedAt: older,
        remoteModifiedAt: older,
      );
      expect(result.resolution, ConflictResolution.useRemote);
    });
  });

  group('LocalAlwaysWinsResolver', () {
    const resolver = LocalAlwaysWinsResolver<_Item>();
    const local = _Item('1', 'Local');
    const remote = _Item('1', 'Remote');
    final t = DateTime(2026, 7, 11);

    test('always returns local regardless of timestamps', () {
      final result = resolver.resolve(
        local: local,
        remote: remote,
        localModifiedAt: t,
        remoteModifiedAt: t.add(const Duration(days: 1)),
      );
      expect(result.resolution, ConflictResolution.useLocal);
      expect(result.resolvedValue, local);
    });
  });

  group('RemoteAlwaysWinsResolver', () {
    const resolver = RemoteAlwaysWinsResolver<_Item>();
    const local = _Item('1', 'Local');
    const remote = _Item('1', 'Remote');
    final t = DateTime(2026, 7, 11);

    test('always returns remote regardless of timestamps', () {
      final result = resolver.resolve(
        local: local,
        remote: remote,
        localModifiedAt: t.add(const Duration(days: 1)),
        remoteModifiedAt: t,
      );
      expect(result.resolution, ConflictResolution.useRemote);
      expect(result.resolvedValue, remote);
    });
  });

  group('Custom ConflictResolver (merge)', () {
    const resolver = _MergeResolver();
    const local = _Item('1', 'A');
    const remote = _Item('1', 'B');
    final t = DateTime(2026, 7, 11);

    test('merge resolver produces merged value', () {
      final result = resolver.resolve(
        local: local,
        remote: remote,
        localModifiedAt: t,
        remoteModifiedAt: t,
      );
      expect(result.resolution, ConflictResolution.merge);
      expect((result.resolvedValue as _Item).name, 'A+B');
    });
  });

  // ── ChangeTracker / InMemoryChangeTracker ─────────────────────────────────

  group('TrackedChange', () {
    final now = DateTime(2026, 7, 11);
    const item = _Item('1', 'Widget');

    test('stores all fields', () {
      final change = TrackedChange<_Item>(
        id: '1',
        value: item,
        state: ChangeState.created,
        changedAt: now,
      );
      expect(change.id, '1');
      expect(change.value, item);
      expect(change.state, ChangeState.created);
      expect(change.changedAt, now);
      expect(change.syncError, isNull);
    });

    test('copyWith replaces specified fields', () {
      final change = TrackedChange<_Item>(
        id: '1',
        value: item,
        state: ChangeState.modified,
        changedAt: now,
      );
      final updated = change.copyWith(state: ChangeState.syncError, syncError: 'err');
      expect(updated.state, ChangeState.syncError);
      expect(updated.syncError, 'err');
      expect(updated.id, '1');
    });

    test('toString includes id, state, changedAt', () {
      final change = TrackedChange<_Item>(
        id: '1',
        value: item,
        state: ChangeState.created,
        changedAt: now,
      );
      expect(change.toString(), contains('1'));
      expect(change.toString(), contains('created'));
    });
  });

  group('InMemoryChangeTracker', () {
    late InMemoryChangeTracker<_Item> tracker;
    const item = _Item('1', 'Widget');
    const updated = _Item('1', 'Widget Pro');

    setUp(() => tracker = InMemoryChangeTracker<_Item>());

    test('trackCreated records created state', () async {
      await tracker.trackCreated('1', item);
      final change = await tracker.getChange('1');
      expect(change?.state, ChangeState.created);
      expect(change?.value, item);
    });

    test('trackModified records modified state', () async {
      await tracker.trackModified('1', updated);
      final change = await tracker.getChange('1');
      expect(change?.state, ChangeState.modified);
    });

    test('trackModified on created record preserves created state', () async {
      await tracker.trackCreated('1', item);
      await tracker.trackModified('1', updated);
      final change = await tracker.getChange('1');
      expect(change?.state, ChangeState.created);
    });

    test('trackDeleted records deleted state', () async {
      await tracker.trackCreated('1', item);
      await tracker.trackDeleted('1');
      final change = await tracker.getChange('1');
      expect(change?.state, ChangeState.deleted);
      expect(change?.value, isNull);
    });

    test('markSynced removes the change', () async {
      await tracker.trackCreated('1', item);
      await tracker.markSynced('1');
      expect(await tracker.getChange('1'), isNull);
    });

    test('markSyncError updates state and error message', () async {
      await tracker.trackModified('1', item);
      await tracker.markSyncError('1', 'network timeout');
      final change = await tracker.getChange('1');
      expect(change?.state, ChangeState.syncError);
      expect(change?.syncError, 'network timeout');
    });

    test('pendingChanges returns all non-synced changes', () async {
      await tracker.trackCreated('1', item);
      await tracker.trackModified('2', updated);
      final pending = await tracker.pendingChanges();
      expect(pending.length, 2);
    });

    test('hasPendingChanges returns true when changes exist', () async {
      await tracker.trackCreated('1', item);
      expect(await tracker.hasPendingChanges, isTrue);
    });

    test('hasPendingChanges returns false when empty', () async {
      expect(await tracker.hasPendingChanges, isFalse);
    });

    test('clear removes all tracked changes', () async {
      await tracker.trackCreated('1', item);
      await tracker.trackModified('2', updated);
      await tracker.clear();
      expect(await tracker.hasPendingChanges, isFalse);
    });

    test('getChange returns null for untracked id', () async {
      expect(await tracker.getChange('unknown'), isNull);
    });

    test('markSyncError on untracked id is a no-op', () async {
      // Should not throw.
      await tracker.markSyncError('unknown', 'error');
      expect(await tracker.hasPendingChanges, isFalse);
    });
  });

  // ── DataMapper / DataMappingException ─────────────────────────────────────

  group('DataMapper (_ItemMapper)', () {
    final mapper = _ItemMapper();

    test('fromRemote maps valid remote DTO to domain object', () {
      final item = mapper.fromRemote({'id': 'i1', 'name': 'Candle'});
      expect(item.id, 'i1');
      expect(item.name, 'Candle');
    });

    test('toRemote maps domain object to remote DTO', () {
      const item = _Item('i1', 'Candle');
      final dto = mapper.toRemote(item);
      expect(dto['id'], 'i1');
      expect(dto['name'], 'Candle');
    });

    test('fromRemoteList maps a list of DTOs', () {
      final items = mapper.fromRemoteList([
        {'id': 'a', 'name': 'A'},
        {'id': 'b', 'name': 'B'},
      ]);
      expect(items.length, 2);
      expect(items[0].id, 'a');
      expect(items[1].name, 'B');
    });

    test('toRemoteList maps a list of domain objects', () {
      const items = [_Item('a', 'A'), _Item('b', 'B')];
      final dtos = mapper.toRemoteList(items);
      expect(dtos.length, 2);
      expect(dtos[0]['id'], 'a');
      expect(dtos[1]['name'], 'B');
    });

    test('fromRemote throws DataMappingException for missing fields', () {
      expect(
        () => mapper.fromRemote({'id': null, 'name': null}),
        throwsA(isA<DataMappingException>()),
      );
    });
  });

  group('DataMappingException', () {
    test('stores field, message, and rawValue', () {
      const ex = DataMappingException(
        field: 'status',
        message: 'Unknown status value',
        rawValue: 'bogus',
      );
      expect(ex.field, 'status');
      expect(ex.message, 'Unknown status value');
      expect(ex.rawValue, 'bogus');
    });

    test('rawValue defaults to null', () {
      const ex = DataMappingException(field: 'id', message: 'Missing id');
      expect(ex.rawValue, isNull);
    });

    test('toString includes field and message', () {
      const ex = DataMappingException(field: 'price', message: 'Negative price');
      expect(ex.toString(), contains('price'));
      expect(ex.toString(), contains('Negative price'));
    });

    test('toString includes rawValue when present', () {
      const ex = DataMappingException(field: 'qty', message: 'Bad qty', rawValue: -5);
      expect(ex.toString(), contains('-5'));
    });

    test('is an Exception', () {
      const ex = DataMappingException(field: 'x', message: 'y');
      expect(ex, isA<Exception>());
    });
  });

  // ── Barrel smoke test ─────────────────────────────────────────────────────

  group('data barrel exports', () {
    test('all public types are accessible via package:data/data.dart', () {
      // LocalCache
      expect(InMemoryCache<String>.new, isNotNull);
      // SyncQueue
      expect(InMemorySyncQueue.new, isNotNull);
      expect(SyncOperationStatus.values, isNotEmpty);
      // ConflictResolver
      expect(LastWriteWinsResolver<String>.new, isNotNull);
      expect(LocalAlwaysWinsResolver<String>.new, isNotNull);
      expect(RemoteAlwaysWinsResolver<String>.new, isNotNull);
      expect(ConflictResolution.values, isNotEmpty);
      // ChangeTracker
      expect(InMemoryChangeTracker<String>.new, isNotNull);
      expect(ChangeState.values, isNotEmpty);
      // DataMapper
      expect(DataMappingException.new, isNotNull);
    });
  });
}
