import 'package:flutter_test/flutter_test.dart';

import 'package:feature_wordpress/feature_wordpress.dart';

void main() {
  late FakeProductPublishingRepository repository;
  late ProductPublishingController controller;

  setUp(() {
    repository = FakeProductPublishingRepository();
    controller = ProductPublishingController(
      GetProductDrafts(repository),
      PublishProduct(repository),
      UpdateProductStatus(repository),
      UpdateProductPrice(repository),
      UpdateProductName(repository),
      UpdateProductDescription(repository),
      UpdateProductCategory(repository),
    );
  });

  tearDown(() {
    controller.dispose();
  });

  group('ProductPublishingController', () {
    test('starts with empty state', () {
      expect(controller.isLoading, false);
      expect(controller.drafts, isEmpty);
      expect(controller.selectedDraft, isNull);
      expect(controller.activeFilter, isNull);
      expect(controller.searchQuery, '');
      expect(controller.error, isNull);
      expect(controller.updatingIds, isEmpty);
    });

    test('load populates drafts and auto-selects first', () async {
      await controller.load();

      expect(controller.isLoading, false);
      expect(controller.drafts.length, 6);
      expect(controller.selectedDraft, isNotNull);
      // Default sort is name ascending; 'Citrus Coaster Set' (id 2) comes first.
      expect(controller.selectedDraft!.id, '2');
      expect(controller.error, isNull);
    });

    test('selectDraft updates selectedDraft', () async {
      await controller.load();

      final third = controller.drafts[2];
      controller.selectDraft(third);

      expect(controller.selectedDraft!.id, third.id);
    });

    test('publish changes draft status and reloads', () async {
      await controller.load();

      // Draft id 4 starts as PublishStatus.draft.
      await controller.publish('4');

      final updated = controller.drafts.firstWhere((d) => d.id == '4');
      expect(updated.status, PublishStatus.published);
    });

    test('setFilter filters by publish status', () async {
      await controller.load();

      controller.setFilter('draft');
      expect(controller.drafts.length, 2);
      expect(controller.drafts.every((d) => d.status == PublishStatus.draft), true);
    });

    test('setFilter with null shows all drafts', () async {
      await controller.load();

      controller.setFilter('published');
      expect(controller.drafts.length, 2);

      controller.setFilter(null);
      expect(controller.drafts.length, 6);
    });

    test('setSearchQuery filters by name', () async {
      await controller.load();

      controller.setSearchQuery('nightlight');
      expect(controller.drafts.length, 1);
      expect(controller.drafts.first.name, 'Ocean Nightlight');
    });

    test('setSearchQuery filters by SKU', () async {
      await controller.load();

      controller.setSearchQuery('JG-BLU');
      expect(controller.drafts.length, 1);
      expect(controller.drafts.first.sku, 'JG-BLU-004');
    });

    test('filter and search combine', () async {
      await controller.load();

      controller.setFilter('published');
      controller.setSearchQuery('floral');
      expect(controller.drafts.length, 1);
      expect(controller.drafts.first.name, 'Floral Bowl Cozy');
    });

    test('selectBySku selects matching draft', () async {
      await controller.load();

      final found = controller.selectBySku('CS-CIT-002');
      expect(found, true);
      expect(controller.selectedDraft!.sku, 'CS-CIT-002');
    });

    test('selectBySku returns false for unknown SKU', () async {
      await controller.load();

      final found = controller.selectBySku('UNKNOWN');
      expect(found, false);
    });

    test('selection is cleared when filtered out', () async {
      await controller.load();

      // Select a published draft.
      controller.selectBySku('BC-FLR-001');
      expect(controller.selectedDraft, isNotNull);

      // Filter to draft status — the selected item should be cleared.
      controller.setFilter('draft');
      expect(controller.selectedDraft, isNull);
    });

    group('updateStatus', () {
      test('draft to published updates status and reloads', () async {
        await controller.load();

        // Product 4 starts as draft.
        await controller.updateStatus('4', PublishStatus.published);

        final updated = controller.drafts.firstWhere((d) => d.id == '4');
        expect(updated.status, PublishStatus.published);
        expect(controller.error, isNull);
        expect(controller.updatingIds, isEmpty);
      });

      test('published to draft updates status and reloads', () async {
        await controller.load();

        // Product 1 starts as published.
        await controller.updateStatus('1', PublishStatus.draft);

        final updated = controller.drafts.firstWhere((d) => d.id == '1');
        expect(updated.status, PublishStatus.draft);
        expect(controller.error, isNull);
        expect(controller.updatingIds, isEmpty);
      });

      test('pendingReview to draft updates status and reloads', () async {
        await controller.load();

        // Product 3 starts as pendingReview.
        await controller.updateStatus('3', PublishStatus.draft);

        final updated = controller.drafts.firstWhere((d) => d.id == '3');
        expect(updated.status, PublishStatus.draft);
        expect(controller.error, isNull);
        expect(controller.updatingIds, isEmpty);
      });

      test('unpublished to draft updates status and reloads', () async {
        await controller.load();

        // Product 6 starts as unpublished.
        await controller.updateStatus('6', PublishStatus.draft);

        final updated = controller.drafts.firstWhere((d) => d.id == '6');
        expect(updated.status, PublishStatus.draft);
        expect(controller.error, isNull);
        expect(controller.updatingIds, isEmpty);
      });

      test('sets lastActionResult on failed update', () async {
        await controller.load();

        // Unknown id triggers StateError in the fake repository.
        await controller.updateStatus('unknown', PublishStatus.draft);

        expect(controller.lastActionResult, isNotNull);
        expect(controller.lastActionResult!.success, isFalse);
        expect(controller.lastActionResult!.targetStatus, PublishStatus.draft);
        expect(controller.lastActionResult!.errorMessage, isNotNull);
        expect(controller.updatingIds, isEmpty);
        // Page-level error should NOT be set for action failures.
        expect(controller.error, isNull);
      });

      test('prevents duplicate calls while row is already updating', () async {
        await controller.load();

        // Start the first update but don't await it yet.
        final first = controller.updateStatus('4', PublishStatus.published);

        // While the first is in flight, the id should be tracked.
        expect(controller.isUpdating('4'), isTrue);

        // A second call for the same id should be silently ignored.
        final second = controller.updateStatus('4', PublishStatus.published);

        // Wait for both to complete.
        await first;
        await second;

        // The product should be updated exactly once.
        final updated = controller.drafts.firstWhere((d) => d.id == '4');
        expect(updated.status, PublishStatus.published);
        expect(controller.updatingIds, isEmpty);
      });

      test('isUpdating returns false for non-updating ids', () async {
        await controller.load();
        expect(controller.isUpdating('4'), isFalse);
      });

      test('does not notify after disposal on async completion', () async {
        await controller.load();

        // Start an update, then immediately dispose.
        final future = controller.updateStatus('4', PublishStatus.published);
        controller.dispose();

        // This should complete without throwing (no notifyListeners after
        // dispose).
        await future;

        // No error should be thrown — the test itself passing is the assertion.
      });
    });

    group('lastActionResult', () {
      test('starts as null', () {
        expect(controller.lastActionResult, isNull);
      });

      test('set to success after updateStatus completes', () async {
        await controller.load();

        await controller.updateStatus('4', PublishStatus.published);

        expect(controller.lastActionResult, isNotNull);
        expect(controller.lastActionResult!.success, isTrue);
        expect(controller.lastActionResult!.productName, 'Fabric Jar Gripper');
        expect(controller.lastActionResult!.targetStatus, PublishStatus.published);
        expect(controller.lastActionResult!.errorMessage, isNull);
      });

      test('set to success after publish completes', () async {
        await controller.load();

        await controller.publish('4');

        expect(controller.lastActionResult, isNotNull);
        expect(controller.lastActionResult!.success, isTrue);
        expect(controller.lastActionResult!.productName, 'Fabric Jar Gripper');
        expect(controller.lastActionResult!.targetStatus, PublishStatus.published);
      });

      test('set to failure with error message on failed publish', () async {
        await controller.load();

        await controller.publish('unknown');

        expect(controller.lastActionResult, isNotNull);
        expect(controller.lastActionResult!.success, isFalse);
        expect(controller.lastActionResult!.errorMessage, isNotNull);
        // Page-level error should NOT be set for action failures.
        expect(controller.error, isNull);
      });

      test('consumeActionResult clears the result', () async {
        await controller.load();

        await controller.updateStatus('4', PublishStatus.published);
        expect(controller.lastActionResult, isNotNull);

        controller.consumeActionResult();
        expect(controller.lastActionResult, isNull);
      });

      test('uses fallback name for unknown product id', () async {
        await controller.load();

        await controller.updateStatus('unknown', PublishStatus.draft);

        expect(controller.lastActionResult!.productName, 'Product unknown');
      });
    });

    group('updatePrice', () {
      test('updates price and reloads', () async {
        await controller.load();

        // Product 4 starts at 8.50.
        await controller.updatePrice('4', 11.99);

        final updated = controller.drafts.firstWhere((d) => d.id == '4');
        expect(updated.price, 11.99);
        expect(controller.error, isNull);
        expect(controller.updatingIds, isEmpty);
      });

      test('sets lastPriceResult on success', () async {
        await controller.load();

        await controller.updatePrice('4', 11.99);

        expect(controller.lastPriceResult, isNotNull);
        expect(controller.lastPriceResult!.success, isTrue);
        expect(controller.lastPriceResult!.productName, 'Fabric Jar Gripper');
        expect(controller.lastPriceResult!.newPrice, 11.99);
        expect(controller.lastPriceResult!.errorMessage, isNull);
      });

      test('sets lastPriceResult on failure', () async {
        await controller.load();

        await controller.updatePrice('unknown', 9.99);

        expect(controller.lastPriceResult, isNotNull);
        expect(controller.lastPriceResult!.success, isFalse);
        expect(controller.lastPriceResult!.errorMessage, isNotNull);
        expect(controller.updatingIds, isEmpty);
        expect(controller.error, isNull);
      });

      test('consumePriceResult clears the result', () async {
        await controller.load();

        await controller.updatePrice('4', 11.99);
        expect(controller.lastPriceResult, isNotNull);

        controller.consumePriceResult();
        expect(controller.lastPriceResult, isNull);
      });

      test('prevents duplicate calls while row is already updating', () async {
        await controller.load();

        final first = controller.updatePrice('4', 11.99);
        expect(controller.isUpdating('4'), isTrue);

        final second = controller.updatePrice('4', 15.00);
        await first;
        await second;

        // Only the first price should have been applied.
        final updated = controller.drafts.firstWhere((d) => d.id == '4');
        expect(updated.price, 11.99);
        expect(controller.updatingIds, isEmpty);
      });
    });

    group('updateName', () {
      test('updates name and reloads', () async {
        await controller.load();

        // Product 4 starts as 'Fabric Jar Gripper'.
        await controller.updateName('4', 'Deluxe Jar Gripper');

        final updated = controller.drafts.firstWhere((d) => d.id == '4');
        expect(updated.name, 'Deluxe Jar Gripper');
        expect(controller.error, isNull);
        expect(controller.updatingIds, isEmpty);
      });

      test('sets lastNameResult on success', () async {
        await controller.load();

        await controller.updateName('4', 'Deluxe Jar Gripper');

        expect(controller.lastNameResult, isNotNull);
        expect(controller.lastNameResult!.success, isTrue);
        expect(controller.lastNameResult!.productName, 'Fabric Jar Gripper');
        expect(controller.lastNameResult!.newName, 'Deluxe Jar Gripper');
        expect(controller.lastNameResult!.errorMessage, isNull);
      });

      test('sets lastNameResult on failure', () async {
        await controller.load();

        await controller.updateName('unknown', 'New Name');

        expect(controller.lastNameResult, isNotNull);
        expect(controller.lastNameResult!.success, isFalse);
        expect(controller.lastNameResult!.errorMessage, isNotNull);
        expect(controller.updatingIds, isEmpty);
        expect(controller.error, isNull);
      });

      test('consumeNameResult clears the result', () async {
        await controller.load();

        await controller.updateName('4', 'Deluxe Jar Gripper');
        expect(controller.lastNameResult, isNotNull);

        controller.consumeNameResult();
        expect(controller.lastNameResult, isNull);
      });

      test('prevents duplicate calls while row is already updating', () async {
        await controller.load();

        final first = controller.updateName('4', 'First Name');
        expect(controller.isUpdating('4'), isTrue);

        final second = controller.updateName('4', 'Second Name');
        await first;
        await second;

        // Only the first name should have been applied.
        final updated = controller.drafts.firstWhere((d) => d.id == '4');
        expect(updated.name, 'First Name');
        expect(controller.updatingIds, isEmpty);
      });
    });

    group('updateDescription', () {
      test('updates description and reloads', () async {
        await controller.load();

        // Product 4 starts with a known description.
        await controller.updateDescription('4', 'A brand new description.');

        final updated = controller.drafts.firstWhere((d) => d.id == '4');
        expect(updated.description, 'A brand new description.');
        expect(controller.error, isNull);
        expect(controller.updatingIds, isEmpty);
      });

      test('sets lastDescriptionResult on success', () async {
        await controller.load();

        await controller.updateDescription('4', 'Updated desc.');

        expect(controller.lastDescriptionResult, isNotNull);
        expect(controller.lastDescriptionResult!.success, isTrue);
        expect(controller.lastDescriptionResult!.productName, 'Fabric Jar Gripper');
        expect(controller.lastDescriptionResult!.errorMessage, isNull);
      });

      test('sets lastDescriptionResult on failure', () async {
        await controller.load();

        await controller.updateDescription('unknown', 'New desc.');

        expect(controller.lastDescriptionResult, isNotNull);
        expect(controller.lastDescriptionResult!.success, isFalse);
        expect(controller.lastDescriptionResult!.errorMessage, isNotNull);
        expect(controller.updatingIds, isEmpty);
        expect(controller.error, isNull);
      });

      test('consumeDescriptionResult clears the result', () async {
        await controller.load();

        await controller.updateDescription('4', 'Updated desc.');
        expect(controller.lastDescriptionResult, isNotNull);

        controller.consumeDescriptionResult();
        expect(controller.lastDescriptionResult, isNull);
      });

      test('prevents duplicate calls while row is already updating', () async {
        await controller.load();

        final first = controller.updateDescription('4', 'First description');
        expect(controller.isUpdating('4'), isTrue);

        final second = controller.updateDescription('4', 'Second description');
        await first;
        await second;

        // Only the first description should have been applied.
        final updated = controller.drafts.firstWhere((d) => d.id == '4');
        expect(updated.description, 'First description');
        expect(controller.updatingIds, isEmpty);
      });
    });

    group('updateCategory', () {
      test('updates category and reloads', () async {
        await controller.load();

        // Product 4 starts with category 'Kitchen Accessories'.
        await controller.updateCategory('4', 'Grippers');

        final updated = controller.drafts.firstWhere((d) => d.id == '4');
        expect(updated.category, 'Grippers');
        expect(controller.error, isNull);
        expect(controller.updatingIds, isEmpty);
      });

      test('sets lastCategoryResult on success', () async {
        await controller.load();

        await controller.updateCategory('4', 'Grippers');

        expect(controller.lastCategoryResult, isNotNull);
        expect(controller.lastCategoryResult!.success, isTrue);
        expect(controller.lastCategoryResult!.productName, 'Fabric Jar Gripper');
        expect(controller.lastCategoryResult!.newCategory, 'Grippers');
        expect(controller.lastCategoryResult!.errorMessage, isNull);
      });

      test('sets lastCategoryResult on failure', () async {
        await controller.load();

        await controller.updateCategory('unknown', 'New Cat');

        expect(controller.lastCategoryResult, isNotNull);
        expect(controller.lastCategoryResult!.success, isFalse);
        expect(controller.lastCategoryResult!.errorMessage, isNotNull);
        expect(controller.updatingIds, isEmpty);
        expect(controller.error, isNull);
      });

      test('consumeCategoryResult clears the result', () async {
        await controller.load();

        await controller.updateCategory('4', 'Grippers');
        expect(controller.lastCategoryResult, isNotNull);

        controller.consumeCategoryResult();
        expect(controller.lastCategoryResult, isNull);
      });

      test('prevents duplicate calls while row is already updating', () async {
        await controller.load();

        final first = controller.updateCategory('4', 'First Category');
        expect(controller.isUpdating('4'), isTrue);

        final second = controller.updateCategory('4', 'Second Category');
        await first;
        await second;

        // Only the first category should have been applied.
        final updated = controller.drafts.firstWhere((d) => d.id == '4');
        expect(updated.category, 'First Category');
        expect(controller.updatingIds, isEmpty);
      });
    });

    // ── Search refinements ──────────────────────────────────────────────

    group('search: category matching', () {
      test('matches by category name', () async {
        await controller.load();

        controller.setSearchQuery('coasters');
        expect(controller.drafts.length, 2);
        expect(controller.drafts.every((d) => d.category == 'Coasters'), true);
      });

      test('matches by category "Kitchen Accessories"', () async {
        await controller.load();

        controller.setSearchQuery('kitchen');
        expect(controller.drafts.length, 2);
        expect(controller.drafts.every((d) => d.category == 'Kitchen Accessories'), true);
      });

      test('category search is case-insensitive', () async {
        await controller.load();

        controller.setSearchQuery('NIGHTLIGHTS');
        expect(controller.drafts.length, 1);
        expect(controller.drafts.first.category, 'Nightlights');
      });
    });

    group('search: multi-word AND tokenization', () {
      test('two-word query matches across fields', () async {
        await controller.load();

        // "bowl cozy" — both tokens appear in "Floral Bowl Cozy"
        controller.setSearchQuery('bowl cozy');
        expect(controller.drafts.length, 1);
        expect(controller.drafts.first.name, 'Floral Bowl Cozy');
      });

      test('tokens can span name and category', () async {
        await controller.load();

        // "fabric kitchen" — "fabric" in name, "kitchen" in category
        controller.setSearchQuery('fabric kitchen');
        expect(controller.drafts.length, 1);
        expect(controller.drafts.first.name, 'Fabric Jar Gripper');
      });

      test('tokens can span name and SKU', () async {
        await controller.load();

        // "ocean NL" — "ocean" in name, "NL" in SKU (NL-OCN-003)
        controller.setSearchQuery('ocean NL');
        expect(controller.drafts.length, 1);
        expect(controller.drafts.first.sku, 'NL-OCN-003');
      });

      test('all tokens must match (AND semantics)', () async {
        await controller.load();

        // "bowl nightlight" — no single product has both
        controller.setSearchQuery('bowl nightlight');
        expect(controller.drafts, isEmpty);
      });

      test('extra whitespace is ignored', () async {
        await controller.load();

        controller.setSearchQuery('  bowl   cozy  ');
        expect(controller.drafts.length, 1);
        expect(controller.drafts.first.name, 'Floral Bowl Cozy');
      });

      test('whitespace-only query shows all products', () async {
        await controller.load();

        controller.setSearchQuery('   ');
        expect(controller.drafts.length, 6);
      });
    });

    // ── Sort refinements ────────────────────────────────────────────────

    group('sort', () {
      test('default sort is name ascending', () async {
        await controller.load();

        expect(controller.activeSortField, ProductSortField.name);
        expect(controller.sortAscending, true);

        final names = controller.drafts.map((d) => d.name).toList();
        expect(names, [
          'Citrus Coaster Set',
          'Fabric Jar Gripper',
          'Floral Bowl Cozy',
          'Ocean Nightlight',
          'Skillet Handle Sleeve',
          'Sublimated Slate Coaster',
        ]);
      });

      test('sort by name descending', () async {
        await controller.load();

        controller.setSort(ProductSortField.name, ascending: false);

        final names = controller.drafts.map((d) => d.name).toList();
        expect(names, [
          'Sublimated Slate Coaster',
          'Skillet Handle Sleeve',
          'Ocean Nightlight',
          'Floral Bowl Cozy',
          'Fabric Jar Gripper',
          'Citrus Coaster Set',
        ]);
      });

      test('sort by lastModified ascending', () async {
        await controller.load();

        controller.setSort(ProductSortField.lastModified, ascending: true);

        final ids = controller.drafts.map((d) => d.id).toList();
        // Dates: id6=Mar20, id2=Mar25, id1=Mar28, id3=Apr1, id4=Apr2, id5=Apr3
        expect(ids, ['6', '2', '1', '3', '4', '5']);
      });

      test('sort by lastModified descending', () async {
        await controller.load();

        controller.setSort(ProductSortField.lastModified, ascending: false);

        final ids = controller.drafts.map((d) => d.id).toList();
        expect(ids, ['5', '4', '3', '1', '2', '6']);
      });

      test('sort by status ascending groups by status order', () async {
        await controller.load();

        controller.setSort(ProductSortField.status, ascending: true);

        final statuses = controller.drafts.map((d) => d.status).toList();
        // draft(0), draft(0), pendingReview(1), published(2), published(2), unpublished(3)
        expect(statuses, [
          PublishStatus.draft,
          PublishStatus.draft,
          PublishStatus.pendingReview,
          PublishStatus.published,
          PublishStatus.published,
          PublishStatus.unpublished,
        ]);

        // Within same status, secondary sort by name ascending.
        final draftNames = controller.drafts
            .where((d) => d.status == PublishStatus.draft)
            .map((d) => d.name)
            .toList();
        expect(draftNames, ['Fabric Jar Gripper', 'Skillet Handle Sleeve']);

        final publishedNames = controller.drafts
            .where((d) => d.status == PublishStatus.published)
            .map((d) => d.name)
            .toList();
        expect(publishedNames, ['Citrus Coaster Set', 'Floral Bowl Cozy']);
      });

      test('sort by status descending reverses status order', () async {
        await controller.load();

        controller.setSort(ProductSortField.status, ascending: false);

        final statuses = controller.drafts.map((d) => d.status).toList();
        expect(statuses, [
          PublishStatus.unpublished,
          PublishStatus.published,
          PublishStatus.published,
          PublishStatus.pendingReview,
          PublishStatus.draft,
          PublishStatus.draft,
        ]);
      });

      test('setSort preserves current ascending when not specified', () async {
        await controller.load();

        controller.setSort(ProductSortField.name, ascending: false);
        expect(controller.sortAscending, false);

        // Change field without specifying ascending — should keep false.
        controller.setSort(ProductSortField.lastModified);
        expect(controller.sortAscending, false);
        expect(controller.activeSortField, ProductSortField.lastModified);
      });

      test('sort persists across filter changes', () async {
        await controller.load();

        controller.setSort(ProductSortField.name, ascending: false);
        controller.setFilter('draft');

        final names = controller.drafts.map((d) => d.name).toList();
        // Two drafts, sorted Z→A
        expect(names, ['Skillet Handle Sleeve', 'Fabric Jar Gripper']);
      });

      test('sort persists across search changes', () async {
        await controller.load();

        controller.setSort(ProductSortField.name, ascending: false);
        controller.setSearchQuery('coaster');

        final names = controller.drafts.map((d) => d.name).toList();
        // Two coaster products, sorted Z→A
        expect(names, ['Sublimated Slate Coaster', 'Citrus Coaster Set']);
      });

      test('sort persists across load cycles', () async {
        await controller.load();

        controller.setSort(ProductSortField.lastModified, ascending: false);

        // Reload — sort settings should be preserved.
        await controller.load();

        expect(controller.activeSortField, ProductSortField.lastModified);
        expect(controller.sortAscending, false);
        final ids = controller.drafts.map((d) => d.id).toList();
        expect(ids, ['5', '4', '3', '1', '2', '6']);
      });
    });

    // ── Persistence ─────────────────────────────────────────────────────

    group('persistence across load', () {
      test('filter persists across load cycles', () async {
        await controller.load();

        controller.setFilter('draft');
        expect(controller.drafts.length, 2);

        await controller.load();

        expect(controller.activeFilter, 'draft');
        expect(controller.drafts.length, 2);
        expect(controller.drafts.every((d) => d.status == PublishStatus.draft), true);
      });

      test('search persists across load cycles', () async {
        await controller.load();

        controller.setSearchQuery('nightlight');
        expect(controller.drafts.length, 1);

        await controller.load();

        expect(controller.searchQuery, 'nightlight');
        expect(controller.drafts.length, 1);
        expect(controller.drafts.first.name, 'Ocean Nightlight');
      });

      test('selection persists across load when still visible', () async {
        await controller.load();

        controller.selectBySku('NL-OCN-003');
        expect(controller.selectedDraft!.sku, 'NL-OCN-003');

        await controller.load();

        expect(controller.selectedDraft, isNotNull);
        expect(controller.selectedDraft!.sku, 'NL-OCN-003');
      });
    });

    // ── Post-write consistency ───────────────────────────────────────────

    group('post-write consistency', () {
      test('selection preserved after status update', () async {
        await controller.load();

        // Select product 4 (draft).
        controller.selectBySku('JG-BLU-004');
        expect(controller.selectedDraft!.id, '4');

        // Publish it.
        await controller.updateStatus('4', PublishStatus.published);

        // Selection should still point to the same product.
        expect(controller.selectedDraft, isNotNull);
        expect(controller.selectedDraft!.id, '4');
        // And reflect the updated status.
        expect(controller.selectedDraft!.status, PublishStatus.published);
      });

      test('selected draft reflects latest price after update', () async {
        await controller.load();

        controller.selectBySku('JG-BLU-004');
        expect(controller.selectedDraft!.price, 8.50);

        await controller.updatePrice('4', 12.00);

        expect(controller.selectedDraft, isNotNull);
        expect(controller.selectedDraft!.id, '4');
        expect(controller.selectedDraft!.price, 12.00);
      });

      test('selected draft reflects latest name after update', () async {
        await controller.load();

        controller.selectBySku('JG-BLU-004');
        expect(controller.selectedDraft!.name, 'Fabric Jar Gripper');

        await controller.updateName('4', 'Premium Jar Gripper');

        expect(controller.selectedDraft, isNotNull);
        expect(controller.selectedDraft!.id, '4');
        expect(controller.selectedDraft!.name, 'Premium Jar Gripper');
      });

      test('selected draft reflects latest description after update', () async {
        await controller.load();

        controller.selectBySku('JG-BLU-004');
        final originalDesc = controller.selectedDraft!.description;

        await controller.updateDescription('4', 'Completely new description.');

        expect(controller.selectedDraft, isNotNull);
        expect(controller.selectedDraft!.id, '4');
        expect(controller.selectedDraft!.description, 'Completely new description.');
        expect(controller.selectedDraft!.description, isNot(originalDesc));
      });

      test('selected draft reflects latest category after update', () async {
        await controller.load();

        controller.selectBySku('JG-BLU-004');
        expect(controller.selectedDraft!.category, 'Kitchen Accessories');

        await controller.updateCategory('4', 'Grippers');

        expect(controller.selectedDraft, isNotNull);
        expect(controller.selectedDraft!.id, '4');
        expect(controller.selectedDraft!.category, 'Grippers');
      });

      test('lastModified updates after write', () async {
        await controller.load();

        controller.selectBySku('JG-BLU-004');
        final originalModified = controller.selectedDraft!.lastModified;

        await controller.updatePrice('4', 15.00);

        expect(controller.selectedDraft, isNotNull);
        expect(controller.selectedDraft!.lastModified.isAfter(originalModified), isTrue);
      });

      test('selection preserved with active filter after write within filter', () async {
        await controller.load();

        // Filter to drafts only.
        controller.setFilter('draft');
        expect(controller.drafts.length, 2);

        // Select product 4 (a draft).
        controller.selectBySku('JG-BLU-004');
        expect(controller.selectedDraft!.id, '4');

        // Update its price — it stays a draft, so it stays in the filter.
        await controller.updatePrice('4', 20.00);

        expect(controller.selectedDraft, isNotNull);
        expect(controller.selectedDraft!.id, '4');
        expect(controller.selectedDraft!.price, 20.00);
        expect(controller.activeFilter, 'draft');
      });

      test('selection moves when write causes item to leave active filter', () async {
        await controller.load();

        // Filter to drafts only.
        controller.setFilter('draft');
        expect(controller.drafts.length, 2);

        // Select product 4 (a draft).
        controller.selectBySku('JG-BLU-004');
        expect(controller.selectedDraft!.id, '4');

        // Publish it — it leaves the 'draft' filter.
        await controller.updateStatus('4', PublishStatus.published);

        // The original selection left the filter; load() auto-selects the
        // first remaining visible draft.
        expect(controller.activeFilter, 'draft');
        expect(controller.drafts.length, 1);
        expect(controller.selectedDraft, isNotNull);
        expect(controller.selectedDraft!.id, isNot('4'));
      });

      test('search persists after write', () async {
        await controller.load();

        controller.setSearchQuery('fabric');
        expect(controller.drafts.length, 1);
        expect(controller.drafts.first.id, '4');

        controller.selectDraft(controller.drafts.first);

        await controller.updatePrice('4', 25.00);

        // Search should still be active.
        expect(controller.searchQuery, 'fabric');
        expect(controller.drafts.length, 1);
        expect(controller.selectedDraft, isNotNull);
        expect(controller.selectedDraft!.price, 25.00);
      });

      test('sort persists after write', () async {
        await controller.load();

        controller.setSort(ProductSortField.lastModified, ascending: false);

        // Select the first item (most recently modified).
        controller.selectDraft(controller.drafts.first);

        // Update a different product's price — this changes its lastModified.
        await controller.updatePrice('1', 99.00);

        // Sort should still be lastModified descending.
        expect(controller.activeSortField, ProductSortField.lastModified);
        expect(controller.sortAscending, false);

        // Product 1 should now be first (most recently modified).
        expect(controller.drafts.first.id, '1');
      });

      test('item repositions under active sort after name change', () async {
        await controller.load();

        // Default sort: name ascending.
        // Order: Citrus, Fabric, Floral, Ocean, Skillet, Sublimated
        expect(controller.drafts.first.name, 'Citrus Coaster Set');

        // Select Fabric Jar Gripper (id 4, currently 2nd).
        controller.selectBySku('JG-BLU-004');
        expect(controller.selectedDraft!.id, '4');

        // Rename to 'Zzz Gripper' — should move to end of list.
        await controller.updateName('4', 'Zzz Gripper');

        // Selection should still be on the same product.
        expect(controller.selectedDraft, isNotNull);
        expect(controller.selectedDraft!.id, '4');
        expect(controller.selectedDraft!.name, 'Zzz Gripper');

        // It should now be last in the sorted list.
        expect(controller.drafts.last.id, '4');
      });
    });

    // ── Multi-select groundwork ─────────────────────────────────────────

    group('multiSelect', () {
      test('starts with empty multi-selection', () {
        expect(controller.multiSelectedIds, isEmpty);
        expect(controller.multiSelectedCount, 0);
        expect(controller.isMultiSelectActive, false);
      });

      test('toggleMultiSelect adds an id', () async {
        await controller.load();

        controller.toggleMultiSelect('1');

        expect(controller.isMultiSelected('1'), true);
        expect(controller.multiSelectedCount, 1);
        expect(controller.isMultiSelectActive, true);
      });

      test('toggleMultiSelect removes an already-selected id', () async {
        await controller.load();

        controller.toggleMultiSelect('1');
        expect(controller.isMultiSelected('1'), true);

        controller.toggleMultiSelect('1');
        expect(controller.isMultiSelected('1'), false);
        expect(controller.multiSelectedCount, 0);
        expect(controller.isMultiSelectActive, false);
      });

      test('multiple ids can be multi-selected', () async {
        await controller.load();

        controller.toggleMultiSelect('1');
        controller.toggleMultiSelect('3');
        controller.toggleMultiSelect('5');

        expect(controller.multiSelectedCount, 3);
        expect(controller.isMultiSelected('1'), true);
        expect(controller.isMultiSelected('3'), true);
        expect(controller.isMultiSelected('5'), true);
        expect(controller.isMultiSelected('2'), false);
      });

      test('clearMultiSelection removes all multi-selected ids', () async {
        await controller.load();

        controller.toggleMultiSelect('1');
        controller.toggleMultiSelect('2');
        controller.toggleMultiSelect('3');
        expect(controller.multiSelectedCount, 3);

        controller.clearMultiSelection();

        expect(controller.multiSelectedIds, isEmpty);
        expect(controller.multiSelectedCount, 0);
        expect(controller.isMultiSelectActive, false);
      });

      test('selectAllVisible selects all visible draft ids', () async {
        await controller.load();

        controller.selectAllVisible();

        expect(controller.multiSelectedCount, 6);
        for (final draft in controller.drafts) {
          expect(controller.isMultiSelected(draft.id), true);
        }
      });

      test('selectAllVisible respects active filter', () async {
        await controller.load();

        controller.setFilter('draft');
        expect(controller.drafts.length, 2);

        controller.selectAllVisible();

        expect(controller.multiSelectedCount, 2);
        expect(controller.isMultiSelected('4'), true);
        expect(controller.isMultiSelected('5'), true);
        // Non-visible ids should not be selected.
        expect(controller.isMultiSelected('1'), false);
      });

      test('multi-select is independent of single-item preview selection', () async {
        await controller.load();

        // Multi-select two items.
        controller.toggleMultiSelect('1');
        controller.toggleMultiSelect('3');

        // Single-select a different item for preview.
        controller.selectDraft(controller.drafts.firstWhere((d) => d.id == '5'));

        // Preview selection should be independent.
        expect(controller.selectedDraft!.id, '5');
        expect(controller.isMultiSelected('5'), false);
        expect(controller.multiSelectedCount, 2);
      });

      test('multi-selection persists across load cycles', () async {
        await controller.load();

        controller.toggleMultiSelect('2');
        controller.toggleMultiSelect('4');

        await controller.load();

        expect(controller.isMultiSelected('2'), true);
        expect(controller.isMultiSelected('4'), true);
        expect(controller.multiSelectedCount, 2);
      });

      test('multi-selection persists after write operations', () async {
        await controller.load();

        controller.toggleMultiSelect('1');
        controller.toggleMultiSelect('4');

        // Update price on product 4.
        await controller.updatePrice('4', 20.00);

        // Multi-selection should still be intact.
        expect(controller.isMultiSelected('1'), true);
        expect(controller.isMultiSelected('4'), true);
        expect(controller.multiSelectedCount, 2);
      });

      test('toggleMultiSelect notifies listeners', () async {
        await controller.load();

        int notifyCount = 0;
        controller.addListener(() => notifyCount++);

        controller.toggleMultiSelect('1');
        expect(notifyCount, 1);

        controller.toggleMultiSelect('1');
        expect(notifyCount, 2);
      });

      test('clearMultiSelection notifies listeners', () async {
        await controller.load();

        controller.toggleMultiSelect('1');

        int notifyCount = 0;
        controller.addListener(() => notifyCount++);

        controller.clearMultiSelection();
        expect(notifyCount, 1);
      });

      test('selectAllVisible notifies listeners', () async {
        await controller.load();

        int notifyCount = 0;
        controller.addListener(() => notifyCount++);

        controller.selectAllVisible();
        expect(notifyCount, 1);
      });

      test('selectAllVisible with search selects only matching products', () async {
        await controller.load();

        controller.setSearchQuery('coaster');
        expect(controller.drafts.length, 2);

        controller.selectAllVisible();

        expect(controller.multiSelectedCount, 2);
        expect(controller.isMultiSelected('2'), true); // Citrus Coaster Set
        expect(controller.isMultiSelected('6'), true); // Sublimated Slate Coaster
        expect(controller.isMultiSelected('1'), false);
      });

      test('selectAllVisible is additive to existing selection', () async {
        await controller.load();

        // Select one product manually.
        controller.toggleMultiSelect('1');

        // Filter to drafts only and select all visible.
        controller.setFilter('draft');
        controller.selectAllVisible();

        // Should have the manually selected + the two drafts.
        expect(controller.isMultiSelected('1'), true);
        expect(controller.isMultiSelected('4'), true);
        expect(controller.isMultiSelected('5'), true);
        expect(controller.multiSelectedCount, 3);
      });
    });

    // ── List density ────────────────────────────────────────────────────

    group('listDensity', () {
      test('defaults to standard', () {
        expect(controller.listDensity, ListDensity.standard);
      });

      test('toggleListDensity switches to compact', () {
        controller.toggleListDensity();
        expect(controller.listDensity, ListDensity.compact);
      });

      test('toggleListDensity switches back to standard', () {
        controller.toggleListDensity();
        expect(controller.listDensity, ListDensity.compact);

        controller.toggleListDensity();
        expect(controller.listDensity, ListDensity.standard);
      });

      test('toggleListDensity notifies listeners', () {
        int notifyCount = 0;
        controller.addListener(() => notifyCount++);

        controller.toggleListDensity();
        expect(notifyCount, 1);
      });

      test('listDensity persists across load cycles', () async {
        controller.toggleListDensity();
        expect(controller.listDensity, ListDensity.compact);

        await controller.load();

        expect(controller.listDensity, ListDensity.compact);
      });
    });

    // ── Staleness detection ─────────────────────────────────────────────

    group('staleness', () {
      test('product modified today is not stale', () async {
        await controller.load();

        final fresh = controller.drafts.first;
        // Use a reference time very close to the fake data dates.
        final now = DateTime(2026, 4, 3);
        expect(controller.isStale(fresh, now: now), isFalse);
      });

      test('product modified 30+ days ago is stale', () async {
        await controller.load();

        // Product 6 has lastModified = 2026-03-20.
        final old = controller.drafts.firstWhere((d) => d.id == '6');
        final now = DateTime(2026, 4, 20); // 31 days later
        expect(controller.isStale(old, now: now), isTrue);
      });

      test('product modified exactly 30 days ago is stale', () async {
        await controller.load();

        // Product 6 has lastModified = 2026-03-20.
        final old = controller.drafts.firstWhere((d) => d.id == '6');
        final now = DateTime(2026, 4, 19); // exactly 30 days
        expect(controller.isStale(old, now: now), isTrue);
      });

      test('product modified 29 days ago is not stale', () async {
        await controller.load();

        // Product 6 has lastModified = 2026-03-20.
        final old = controller.drafts.firstWhere((d) => d.id == '6');
        final now = DateTime(2026, 4, 18); // 29 days
        expect(controller.isStale(old, now: now), isFalse);
      });

      test('staleCount returns correct count', () async {
        await controller.load();

        // With now = 2026-05-01, all 6 products have lastModified in March/April.
        // All are older than 30 days from May 1.
        // Dates: Mar20, Mar25, Mar28, Apr1, Apr2, Apr3
        // Days from May 1: 42, 37, 34, 30, 29, 28
        // Stale (>=30): Mar20(42), Mar25(37), Mar28(34), Apr1(30) = 4
        final now = DateTime(2026, 5, 1);
        expect(controller.staleCount(now: now), 4);
      });

      test('staleCount respects active filter', () async {
        await controller.load();

        // Filter to drafts only (ids 4 and 5, dates Apr2 and Apr3).
        controller.setFilter('draft');

        // With now = 2026-05-03: Apr2 = 31 days (stale), Apr3 = 30 days (stale)
        final now = DateTime(2026, 5, 3);
        expect(controller.staleCount(now: now), 2);
      });

      test('freshly written product is no longer stale', () async {
        await controller.load();

        // Product 6 (Mar20) is stale as of May 1.
        final now = DateTime(2026, 5, 1);
        final oldProduct = controller.drafts.firstWhere((d) => d.id == '6');
        expect(controller.isStale(oldProduct, now: now), isTrue);

        // Update its price — the fake repo sets lastModified to DateTime.now().
        await controller.updatePrice('6', 20.00);

        // After the write, the product has a fresh lastModified.
        final updated = controller.drafts.firstWhere((d) => d.id == '6');
        expect(controller.isStale(updated), isFalse);
      });
    });

    // ── Keyboard navigation ─────────────────────────────────────────────

    group('keyboard navigation', () {
      test('selectNextDraft moves to next item', () async {
        await controller.load();

        // Default sort: name ascending. First selected = Citrus Coaster Set (id 2).
        expect(controller.selectedDraft!.id, '2');

        controller.selectNextDraft();
        // Next: Fabric Jar Gripper (id 4).
        expect(controller.selectedDraft!.id, '4');
      });

      test('selectNextDraft wraps to first at end of list', () async {
        await controller.load();

        // Select last item: Sublimated Slate Coaster (id 6).
        controller.selectDraft(controller.drafts.last);
        expect(controller.selectedDraft!.id, '6');

        controller.selectNextDraft();
        // Should wrap to first: Citrus Coaster Set (id 2).
        expect(controller.selectedDraft!.id, '2');
      });

      test('selectPreviousDraft moves to previous item', () async {
        await controller.load();

        // Select 2nd item: Fabric Jar Gripper (id 4).
        controller.selectDraft(controller.drafts[1]);
        expect(controller.selectedDraft!.id, '4');

        controller.selectPreviousDraft();
        // Previous: Citrus Coaster Set (id 2).
        expect(controller.selectedDraft!.id, '2');
      });

      test('selectPreviousDraft wraps to last at beginning of list', () async {
        await controller.load();

        // Already at first item: Citrus Coaster Set (id 2).
        expect(controller.selectedDraft!.id, '2');

        controller.selectPreviousDraft();
        // Should wrap to last: Sublimated Slate Coaster (id 6).
        expect(controller.selectedDraft!.id, '6');
      });

      test('selectNextDraft selects first when no selection', () async {
        await controller.load();
        controller.selectedDraft = null;

        final result = controller.selectNextDraft();
        expect(result, isTrue);
        expect(controller.selectedDraft!.id, '2'); // First in list.
      });

      test('selectPreviousDraft selects last when no selection', () async {
        await controller.load();
        controller.selectedDraft = null;

        final result = controller.selectPreviousDraft();
        expect(result, isTrue);
        expect(controller.selectedDraft!.id, '6'); // Last in list.
      });

      test('selectNextDraft returns false on empty list', () async {
        await controller.load();
        controller.setSearchQuery('zzz_no_match');

        final result = controller.selectNextDraft();
        expect(result, isFalse);
      });

      test('selectPreviousDraft returns false on empty list', () async {
        await controller.load();
        controller.setSearchQuery('zzz_no_match');

        final result = controller.selectPreviousDraft();
        expect(result, isFalse);
      });

      test('selectNextDraft notifies listeners', () async {
        await controller.load();

        int notifyCount = 0;
        controller.addListener(() => notifyCount++);

        controller.selectNextDraft();
        expect(notifyCount, 1);
      });

      test('keyboard navigation respects active filter', () async {
        await controller.load();

        // Filter to drafts only (ids 4 and 5, name-sorted: Fabric, Skillet).
        controller.setFilter('draft');
        expect(controller.drafts.length, 2);

        // Auto-selection cleared or set to first visible.
        controller.selectDraft(controller.drafts.first);
        expect(controller.selectedDraft!.id, '4');

        controller.selectNextDraft();
        expect(controller.selectedDraft!.id, '5');

        controller.selectNextDraft();
        // Wraps back to first.
        expect(controller.selectedDraft!.id, '4');
      });
    });

    // ── Bulk status actions ──────────────────────────────────────────────

    group('bulkUpdateStatus', () {
      test('does nothing when no items are multi-selected', () async {
        await controller.load();

        await controller.bulkUpdateStatus(PublishStatus.draft);

        // No result should be set.
        expect(controller.lastBulkActionResult, isNull);
      });

      test('moves all selected products to draft', () async {
        await controller.load();

        // Select two published products (ids 1 and 2).
        controller.toggleMultiSelect('1');
        controller.toggleMultiSelect('2');
        expect(controller.multiSelectedCount, 2);

        await controller.bulkUpdateStatus(PublishStatus.draft);

        // Both should now be draft.
        final p1 = controller.drafts.firstWhere((d) => d.id == '1');
        final p2 = controller.drafts.firstWhere((d) => d.id == '2');
        expect(p1.status, PublishStatus.draft);
        expect(p2.status, PublishStatus.draft);
      });

      test('sets lastBulkActionResult on all-success', () async {
        await controller.load();

        controller.toggleMultiSelect('1');
        controller.toggleMultiSelect('3');
        await controller.bulkUpdateStatus(PublishStatus.draft);

        expect(controller.lastBulkActionResult, isNotNull);
        expect(controller.lastBulkActionResult!.successCount, 2);
        expect(controller.lastBulkActionResult!.failureCount, 0);
        expect(controller.lastBulkActionResult!.targetStatus, PublishStatus.draft);
        expect(controller.lastBulkActionResult!.allSucceeded, isTrue);
        expect(controller.lastBulkActionResult!.totalCount, 2);
        expect(controller.lastBulkActionResult!.failedProductNames, isEmpty);
      });

      test('clears multi-selection after bulk action', () async {
        await controller.load();

        controller.toggleMultiSelect('1');
        controller.toggleMultiSelect('2');
        controller.toggleMultiSelect('3');
        expect(controller.multiSelectedCount, 3);

        await controller.bulkUpdateStatus(PublishStatus.draft);

        expect(controller.multiSelectedIds, isEmpty);
        expect(controller.multiSelectedCount, 0);
        expect(controller.isMultiSelectActive, false);
      });

      test('clears updatingIds after bulk action', () async {
        await controller.load();

        controller.toggleMultiSelect('1');
        controller.toggleMultiSelect('2');

        await controller.bulkUpdateStatus(PublishStatus.draft);

        expect(controller.updatingIds, isEmpty);
      });

      test('handles mixed statuses — all become draft', () async {
        await controller.load();

        // Select one of each status type.
        controller.toggleMultiSelect('1'); // published
        controller.toggleMultiSelect('3'); // pendingReview
        controller.toggleMultiSelect('4'); // draft (already)
        controller.toggleMultiSelect('6'); // unpublished

        await controller.bulkUpdateStatus(PublishStatus.draft);

        for (final id in ['1', '3', '4', '6']) {
          final product = controller.drafts.firstWhere((d) => d.id == id);
          expect(product.status, PublishStatus.draft, reason: 'Product $id should be draft');
        }
        expect(controller.lastBulkActionResult!.successCount, 4);
      });

      test('consumeBulkActionResult clears the result', () async {
        await controller.load();

        controller.toggleMultiSelect('1');
        await controller.bulkUpdateStatus(PublishStatus.draft);
        expect(controller.lastBulkActionResult, isNotNull);

        controller.consumeBulkActionResult();
        expect(controller.lastBulkActionResult, isNull);
      });

      test('lastBulkActionResult starts as null', () {
        expect(controller.lastBulkActionResult, isNull);
      });

      test('preserves single-item preview selection after bulk action', () async {
        await controller.load();

        // Select product 5 for preview.
        controller.selectBySku('SH-SUN-005');
        expect(controller.selectedDraft!.id, '5');

        // Multi-select other products.
        controller.toggleMultiSelect('1');
        controller.toggleMultiSelect('2');

        await controller.bulkUpdateStatus(PublishStatus.draft);

        // Preview selection should still be on product 5.
        expect(controller.selectedDraft, isNotNull);
        expect(controller.selectedDraft!.id, '5');
      });

      test('filter and sort persist after bulk action', () async {
        await controller.load();

        controller.setSort(ProductSortField.lastModified, ascending: false);

        controller.toggleMultiSelect('1');
        controller.toggleMultiSelect('2');

        await controller.bulkUpdateStatus(PublishStatus.draft);

        expect(controller.activeSortField, ProductSortField.lastModified);
        expect(controller.sortAscending, false);
      });

      test('does not notify after disposal during bulk action', () async {
        await controller.load();

        controller.toggleMultiSelect('1');
        controller.toggleMultiSelect('2');

        final future = controller.bulkUpdateStatus(PublishStatus.draft);
        controller.dispose();

        // Should complete without throwing.
        await future;
      });
    });

    group('disposed guard', () {
      test('load does not notify after disposal', () async {
        // Start load, then immediately dispose.
        final future = controller.load();
        controller.dispose();

        // Should complete without throwing.
        await future;
      });

      test('publish does not notify after disposal', () async {
        await controller.load();

        final future = controller.publish('4');
        controller.dispose();

        // Should complete without throwing.
        await future;
      });
    });
  });
}
