import 'package:flutter_test/flutter_test.dart';

import 'package:feature_wordpress/feature_wordpress.dart';
import 'package:feature_wordpress/src/application/get_product_drafts.dart';
import 'package:feature_wordpress/src/application/product_publishing_controller.dart';
import 'package:feature_wordpress/src/application/publish_product.dart';

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

  setUp(() {
    repository = FakeProductPublishingRepository();
    controller = ProductPublishingController(
      GetProductDrafts(repository),
      PublishProduct(repository),
      UpdateProductStatus(repository),
      UpdateProductPrice(repository),
      UpdateProductName(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);
      });
    });

    // ── 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');
      });
    });

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