import { AddUrlToLibraryUseCase } from '../../application/useCases/commands/AddUrlToLibraryUseCase';
import { InMemoryCardRepository } from '../utils/InMemoryCardRepository';
import { InMemoryCollectionRepository } from '../utils/InMemoryCollectionRepository';
import { FakeCardPublisher } from '../utils/FakeCardPublisher';
import { FakeCollectionPublisher } from '../utils/FakeCollectionPublisher';
import { FakeMetadataService } from '../utils/FakeMetadataService';
import { CardLibraryService } from '../../domain/services/CardLibraryService';
import { CardCollectionService } from '../../domain/services/CardCollectionService';
import { CuratorId } from '../../domain/value-objects/CuratorId';
import { CollectionBuilder } from '../utils/builders/CollectionBuilder';
import { CardTypeEnum } from '../../domain/value-objects/CardType';
import { FakeEventPublisher } from '../utils/FakeEventPublisher';
import { CardAddedToLibraryEvent } from '../../domain/events/CardAddedToLibraryEvent';
import { CardAddedToCollectionEvent } from '../../domain/events/CardAddedToCollectionEvent';
import { EventNames } from 'src/shared/infrastructure/events/EventConfig';

describe('AddUrlToLibraryUseCase', () => {
  let useCase: AddUrlToLibraryUseCase;
  let cardRepository: InMemoryCardRepository;
  let collectionRepository: InMemoryCollectionRepository;
  let cardPublisher: FakeCardPublisher;
  let collectionPublisher: FakeCollectionPublisher;
  let metadataService: FakeMetadataService;
  let cardLibraryService: CardLibraryService;
  let cardCollectionService: CardCollectionService;
  let eventPublisher: FakeEventPublisher;
  let curatorId: CuratorId;

  beforeEach(() => {
    cardRepository = InMemoryCardRepository.getInstance();
    collectionRepository = InMemoryCollectionRepository.getInstance();
    cardPublisher = new FakeCardPublisher();
    collectionPublisher = new FakeCollectionPublisher();
    metadataService = new FakeMetadataService();
    eventPublisher = new FakeEventPublisher();

    cardLibraryService = new CardLibraryService(
      cardRepository,
      cardPublisher,
      collectionRepository,
      cardCollectionService,
    );
    cardCollectionService = new CardCollectionService(
      collectionRepository,
      collectionPublisher,
      cardRepository,
    );

    useCase = new AddUrlToLibraryUseCase(
      cardRepository,
      metadataService,
      cardLibraryService,
      cardCollectionService,
      eventPublisher,
    );

    curatorId = CuratorId.create('did:plc:testcurator').unwrap();
  });

  afterEach(() => {
    cardRepository.clear();
    collectionRepository.clear();
    cardPublisher.clear();
    collectionPublisher.clear();
    metadataService.clear();
    eventPublisher.clear();
  });

  describe('Basic URL card creation', () => {
    it('should create and add a URL card to library', async () => {
      const request = {
        url: 'https://example.com/article',
        curatorId: curatorId.value,
      };

      const result = await useCase.execute(request);

      expect(result.isOk()).toBe(true);
      const response = result.unwrap();
      expect(response.urlCardId).toBeDefined();
      expect(response.noteCardId).toBeUndefined();

      // Verify card was saved
      const savedCards = cardRepository.getAllCards();
      expect(savedCards).toHaveLength(1);
      expect(savedCards[0]!.content.type).toBe(CardTypeEnum.URL);

      // Verify card was published to library
      const publishedCards = cardPublisher.getPublishedCards();
      expect(publishedCards).toHaveLength(1);

      // Verify CardAddedToLibraryEvent was published
      const libraryEvents = eventPublisher.getPublishedEventsOfType(
        EventNames.CARD_ADDED_TO_LIBRARY,
      ) as CardAddedToLibraryEvent[];
      expect(libraryEvents).toHaveLength(1);
      expect(libraryEvents[0]?.cardId.getStringValue()).toBe(
        response.urlCardId,
      );
      expect(libraryEvents[0]?.curatorId.equals(curatorId)).toBe(true);
    });

    it('should create URL card with note when note is provided', async () => {
      const request = {
        url: 'https://example.com/article',
        note: 'This is a great article about testing',
        curatorId: curatorId.value,
      };

      const result = await useCase.execute(request);

      expect(result.isOk()).toBe(true);
      const response = result.unwrap();
      expect(response.urlCardId).toBeDefined();
      expect(response.noteCardId).toBeDefined();

      // Verify both cards were saved
      const savedCards = cardRepository.getAllCards();
      expect(savedCards).toHaveLength(2);

      const urlCard = savedCards.find(
        (card) => card.content.type === CardTypeEnum.URL,
      );
      const noteCard = savedCards.find(
        (card) => card.content.type === CardTypeEnum.NOTE,
      );

      expect(urlCard).toBeDefined();
      expect(noteCard).toBeDefined();
      expect(noteCard?.parentCardId?.getStringValue()).toBe(
        urlCard?.cardId.getStringValue(),
      );

      // Verify both cards were published to library
      const publishedCards = cardPublisher.getPublishedCards();
      expect(publishedCards).toHaveLength(2);

      // Verify CardAddedToLibraryEvent was published for only URL card
      const libraryEvents = eventPublisher.getPublishedEventsOfType(
        EventNames.CARD_ADDED_TO_LIBRARY,
      ) as CardAddedToLibraryEvent[];
      expect(libraryEvents).toHaveLength(1);

      const urlCardEvent = libraryEvents.find(
        (event) =>
          event.cardId.getStringValue() === urlCard?.cardId.getStringValue(),
      );

      expect(urlCardEvent).toBeDefined();
      expect(urlCardEvent?.curatorId.equals(curatorId)).toBe(true);
    });
  });

  describe('Existing URL card handling', () => {
    it('should reuse existing URL card instead of creating new one', async () => {
      const url = 'https://example.com/existing';

      // First request creates the URL card
      const firstRequest = {
        url,
        curatorId: curatorId.value,
      };

      const firstResult = await useCase.execute(firstRequest);
      expect(firstResult.isOk()).toBe(true);
      const firstResponse = firstResult.unwrap();

      // Second request should reuse the same URL card
      const secondRequest = {
        url,
        note: 'Adding a note to existing URL',
        curatorId: curatorId.value,
      };

      const secondResult = await useCase.execute(secondRequest);
      expect(secondResult.isOk()).toBe(true);
      const secondResponse = secondResult.unwrap();

      // Should have same URL card ID
      expect(secondResponse.urlCardId).toBe(firstResponse.urlCardId);
      expect(secondResponse.noteCardId).toBeDefined();

      // Should have URL card + note card
      const savedCards = cardRepository.getAllCards();
      expect(savedCards).toHaveLength(2);

      const urlCards = savedCards.filter(
        (card) => card.content.type === CardTypeEnum.URL,
      );
      expect(urlCards).toHaveLength(1); // Only one URL card
    });

    it('should create new URL card when another user has URL card with same URL', async () => {
      const url = 'https://example.com/shared';
      const otherCuratorId = CuratorId.create('did:plc:othercurator').unwrap();

      // First user creates URL card
      const firstRequest = {
        url,
        curatorId: otherCuratorId.value,
      };

      const firstResult = await useCase.execute(firstRequest);
      expect(firstResult.isOk()).toBe(true);
      const firstResponse = firstResult.unwrap();

      // Second user (different curator) should create their own URL card
      const secondRequest = {
        url,
        curatorId: curatorId.value,
      };

      const secondResult = await useCase.execute(secondRequest);
      expect(secondResult.isOk()).toBe(true);
      const secondResponse = secondResult.unwrap();

      // Should have different URL card IDs
      expect(secondResponse.urlCardId).not.toBe(firstResponse.urlCardId);

      // Should have two separate URL cards
      const savedCards = cardRepository.getAllCards();
      expect(savedCards).toHaveLength(2);

      const urlCards = savedCards.filter(
        (card) => card.content.type === CardTypeEnum.URL,
      );
      expect(urlCards).toHaveLength(2); // Two separate URL cards

      // Verify each card belongs to the correct curator
      const firstUserCard = urlCards.find((card) =>
        card.props.curatorId.equals(otherCuratorId),
      );
      const secondUserCard = urlCards.find((card) =>
        card.props.curatorId.equals(curatorId),
      );

      expect(firstUserCard).toBeDefined();
      expect(secondUserCard).toBeDefined();
    });

    it('should create new URL card when no one has URL card with that URL yet', async () => {
      const url = 'https://example.com/brand-new';

      // Verify no cards exist initially
      expect(cardRepository.getAllCards()).toHaveLength(0);

      const request = {
        url,
        curatorId: curatorId.value,
      };

      const result = await useCase.execute(request);

      expect(result.isOk()).toBe(true);
      const response = result.unwrap();
      expect(response.urlCardId).toBeDefined();

      // Verify new URL card was created
      const savedCards = cardRepository.getAllCards();
      expect(savedCards).toHaveLength(1);

      const urlCard = savedCards[0];
      expect(urlCard?.content.type).toBe(CardTypeEnum.URL);
      expect(urlCard?.props.curatorId.equals(curatorId)).toBe(true);
    });

    it('should update existing note card when URL already exists with a note', async () => {
      const url = 'https://example.com/existing';

      // First request creates URL card with note
      const firstRequest = {
        url,
        note: 'Original note',
        curatorId: curatorId.value,
      };

      const firstResult = await useCase.execute(firstRequest);
      expect(firstResult.isOk()).toBe(true);
      const firstResponse = firstResult.unwrap();
      expect(firstResponse.noteCardId).toBeDefined();

      // Get the original note card
      const cardsAfterFirst = cardRepository.getAllCards();
      const originalNoteCard = cardsAfterFirst.find(
        (card) => card.content.type === CardTypeEnum.NOTE,
      );
      expect(originalNoteCard).toBeDefined();
      expect(originalNoteCard?.content.noteContent?.text).toBe('Original note');

      // Second request updates the note
      const secondRequest = {
        url,
        note: 'Updated note',
        curatorId: curatorId.value,
      };

      const secondResult = await useCase.execute(secondRequest);
      expect(secondResult.isOk()).toBe(true);
      const secondResponse = secondResult.unwrap();

      // Should still have the same note card ID
      expect(secondResponse.noteCardId).toBe(firstResponse.noteCardId);

      // Should still have 2 cards (URL + Note)
      const savedCards = cardRepository.getAllCards();
      expect(savedCards).toHaveLength(2);

      // Verify note was updated
      const updatedNoteCard = savedCards.find(
        (card) => card.content.type === CardTypeEnum.NOTE,
      );
      expect(updatedNoteCard).toBeDefined();
      expect(updatedNoteCard?.content.noteContent?.text).toBe('Updated note');
      expect(updatedNoteCard?.cardId.getStringValue()).toBe(
        firstResponse.noteCardId,
      );
    });
  });

  describe('Collection handling', () => {
    it('should add URL card to specified collections', async () => {
      // Create a test collection
      const collection = new CollectionBuilder()
        .withAuthorId(curatorId.value)
        .withName('Test Collection')
        .build();

      if (collection instanceof Error) {
        throw new Error(`Failed to create collection: ${collection.message}`);
      }

      await collectionRepository.save(collection);

      const request = {
        url: 'https://example.com/article',
        collectionIds: [collection.collectionId.getStringValue()],
        curatorId: curatorId.value,
      };

      const result = await useCase.execute(request);

      expect(result.isOk()).toBe(true);

      // Verify collection link was published
      const publishedLinks = collectionPublisher.getPublishedLinksForCollection(
        collection.collectionId.getStringValue(),
      );
      expect(publishedLinks).toHaveLength(1);

      // Verify CardAddedToLibraryEvent was published
      const libraryEvents = eventPublisher.getPublishedEventsOfType(
        EventNames.CARD_ADDED_TO_LIBRARY,
      ) as CardAddedToLibraryEvent[];
      expect(libraryEvents).toHaveLength(1);
      expect(libraryEvents[0]?.curatorId.equals(curatorId)).toBe(true);

      // Verify CardAddedToCollectionEvent was published
      const collectionEvents = eventPublisher.getPublishedEventsOfType(
        EventNames.CARD_ADDED_TO_COLLECTION,
      ) as CardAddedToCollectionEvent[];
      expect(collectionEvents).toHaveLength(1);
      expect(collectionEvents[0]?.collectionId.getStringValue()).toBe(
        collection.collectionId.getStringValue(),
      );
      expect(collectionEvents[0]?.addedBy.equals(curatorId)).toBe(true);
    });

    it('should add URL card to collections with viaCardId', async () => {
      // Create a test collection
      const collection = new CollectionBuilder()
        .withAuthorId(curatorId.value)
        .withName('Test Collection')
        .build();

      if (collection instanceof Error) {
        throw new Error(`Failed to create collection: ${collection.message}`);
      }

      await collectionRepository.save(collection);

      // Create a via card first
      const viaCardRequest = {
        url: 'https://example.com/via-article',
        curatorId: curatorId.value,
      };

      const viaCardResult = await useCase.execute(viaCardRequest);
      expect(viaCardResult.isOk()).toBe(true);
      const viaCardId = viaCardResult.unwrap().urlCardId;

      // Now add URL with viaCardId
      const request = {
        url: 'https://example.com/article',
        collectionIds: [collection.collectionId.getStringValue()],
        viaCardId: viaCardId,
        curatorId: curatorId.value,
      };

      const result = await useCase.execute(request);

      expect(result.isOk()).toBe(true);
      const response = result.unwrap();

      // Verify URL card was created
      expect(response.urlCardId).toBeDefined();
      expect(response.urlCardId).not.toBe(viaCardId);

      // Verify collection link was published
      const publishedLinks = collectionPublisher.getPublishedLinksForCollection(
        collection.collectionId.getStringValue(),
      );
      expect(publishedLinks).toHaveLength(1);

      // Verify the published link is for the new URL card
      const publishedLink = publishedLinks[0];
      expect(publishedLink?.cardId).toBe(response.urlCardId);

      // Verify CardAddedToCollectionEvent was published with correct viaCardId
      const collectionEvents = eventPublisher.getPublishedEventsOfType(
        EventNames.CARD_ADDED_TO_COLLECTION,
      ) as CardAddedToCollectionEvent[];
      expect(collectionEvents).toHaveLength(1);
      expect(collectionEvents[0]?.cardId.getStringValue()).toBe(
        response.urlCardId,
      );
      expect(collectionEvents[0]?.collectionId.getStringValue()).toBe(
        collection.collectionId.getStringValue(),
      );
      expect(collectionEvents[0]?.addedBy.equals(curatorId)).toBe(true);
    });

    it('should add URL card (not note card) to collections when note is provided', async () => {
      // Create a test collection
      const collection = new CollectionBuilder()
        .withAuthorId(curatorId.value)
        .withName('Test Collection')
        .build();

      if (collection instanceof Error) {
        throw new Error(`Failed to create collection: ${collection.message}`);
      }

      await collectionRepository.save(collection);

      const request = {
        url: 'https://example.com/article',
        note: 'This is my note about the article',
        collectionIds: [collection.collectionId.getStringValue()],
        curatorId: curatorId.value,
      };

      const result = await useCase.execute(request);

      expect(result.isOk()).toBe(true);
      const response = result.unwrap();

      // Verify both URL and note cards were created
      expect(response.urlCardId).toBeDefined();
      expect(response.noteCardId).toBeDefined();

      // Verify both cards were saved
      const savedCards = cardRepository.getAllCards();
      expect(savedCards).toHaveLength(2);

      const urlCard = savedCards.find(
        (card) => card.content.type === CardTypeEnum.URL,
      );
      const noteCard = savedCards.find(
        (card) => card.content.type === CardTypeEnum.NOTE,
      );

      expect(urlCard).toBeDefined();
      expect(noteCard).toBeDefined();

      // Verify collection link was published for URL card only
      const publishedLinks = collectionPublisher.getPublishedLinksForCollection(
        collection.collectionId.getStringValue(),
      );
      expect(publishedLinks).toHaveLength(1);

      // Verify the published link is for the URL card, not the note card
      const publishedLink = publishedLinks[0];
      expect(publishedLink?.cardId).toBe(urlCard?.cardId.getStringValue());
      expect(publishedLink?.cardId).not.toBe(noteCard?.cardId.getStringValue());

      // Verify both cards are in the library
      const publishedCards = cardPublisher.getPublishedCards();
      expect(publishedCards).toHaveLength(2);

      // Verify CardAddedToLibraryEvent was published for both cards
      const libraryEvents = eventPublisher.getPublishedEventsOfType(
        EventNames.CARD_ADDED_TO_LIBRARY,
      ) as CardAddedToLibraryEvent[];
      expect(libraryEvents).toHaveLength(1);

      // Verify CardAddedToCollectionEvent was published for URL card only
      const collectionEvents = eventPublisher.getPublishedEventsOfType(
        EventNames.CARD_ADDED_TO_COLLECTION,
      ) as CardAddedToCollectionEvent[];
      expect(collectionEvents).toHaveLength(1);
      expect(collectionEvents[0]?.cardId.getStringValue()).toBe(
        urlCard?.cardId.getStringValue(),
      );
      expect(collectionEvents[0]?.collectionId.getStringValue()).toBe(
        collection.collectionId.getStringValue(),
      );
      expect(collectionEvents[0]?.addedBy.equals(curatorId)).toBe(true);
    });

    it('should fail when collection does not exist', async () => {
      const request = {
        url: 'https://example.com/article',
        collectionIds: ['non-existent-collection-id'],
        curatorId: curatorId.value,
      };

      const result = await useCase.execute(request);

      expect(result.isErr()).toBe(true);
      if (result.isErr()) {
        expect(result.error.message).toContain('Collection not found');
      }
    });
  });

  describe('Validation', () => {
    it('should fail with invalid URL', async () => {
      const request = {
        url: 'not-a-valid-url',
        curatorId: curatorId.value,
      };

      const result = await useCase.execute(request);

      expect(result.isErr()).toBe(true);
      if (result.isErr()) {
        expect(result.error.message).toContain('Invalid URL');
      }
    });

    it('should fail with invalid curator ID', async () => {
      const request = {
        url: 'https://example.com/article',
        curatorId: 'invalid-curator-id',
      };

      const result = await useCase.execute(request);

      expect(result.isErr()).toBe(true);
      if (result.isErr()) {
        expect(result.error.message).toContain('Invalid curator ID');
      }
    });

    it('should fail with invalid collection ID', async () => {
      const request = {
        url: 'https://example.com/article',
        collectionIds: ['invalid-collection-id'],
        curatorId: curatorId.value,
      };

      const result = await useCase.execute(request);

      expect(result.isErr()).toBe(true);
      if (result.isErr()) {
        expect(result.error.message).toContain('Collection not found');
      }
    });
  });

  describe('Publishing and metadata integration', () => {
    it('should handle metadata service failure gracefully', async () => {
      // Configure metadata service to fail
      metadataService.setShouldFail(true);

      const request = {
        url: 'https://example.com/article',
        curatorId: curatorId.value,
      };

      const result = await useCase.execute(request);

      expect(result.isOk()).toBe(true);
    });

    it('should not save cards when publishing to library fails', async () => {
      // Configure card publisher to fail
      cardPublisher.setShouldFail(true);

      const request = {
        url: 'https://example.com/article',
        curatorId: curatorId.value,
      };

      const result = await useCase.execute(request);

      expect(result.isErr()).toBe(true);
      if (result.isErr()) {
        expect(result.error.message).toContain('Failed to publish card');
      }

      // Verify no cards were saved when publishing failed
      const savedCards = cardRepository.getAllCards();
      expect(savedCards).toHaveLength(0);

      // Verify no cards were published
      const publishedCards = cardPublisher.getPublishedCards();
      expect(publishedCards).toHaveLength(0);

      // Verify no events were published
      const libraryEvents = eventPublisher.getPublishedEventsOfType(
        EventNames.CARD_ADDED_TO_LIBRARY,
      );
      expect(libraryEvents).toHaveLength(0);
    });

    it('should not save cards when collection publishing fails', async () => {
      // Create a test collection
      const collection = new CollectionBuilder()
        .withAuthorId(curatorId.value)
        .withName('Test Collection')
        .build();

      if (collection instanceof Error) {
        throw new Error(`Failed to create collection: ${collection.message}`);
      }

      await collectionRepository.save(collection);

      // Configure collection publisher to fail (for card-collection links)
      collectionPublisher.setShouldFail(true);

      const request = {
        url: 'https://example.com/article',
        collectionIds: [collection.collectionId.getStringValue()],
        curatorId: curatorId.value,
      };

      const result = await useCase.execute(request);

      expect(result.isErr()).toBe(true);
      if (result.isErr()) {
        expect(result.error.message).toContain('Failed to publish');
      }

      // card should still be published to library
      const savedCards = cardRepository.getAllCards();
      expect(savedCards).toHaveLength(1);

      const savedCollection = await collectionRepository.findById(
        collection.collectionId,
      );
      if (savedCollection.isErr()) {
        throw new Error(
          `Failed to retrieve collection: ${savedCollection.error.message}`,
        );
      }
      expect(savedCollection.value?.cardCount).toBe(0); // card count should be 0 since link publishing failed

      // Verify no collection links were published
      const publishedLinks = collectionPublisher.getPublishedLinksForCollection(
        collection.collectionId.getStringValue(),
      );
      expect(publishedLinks).toHaveLength(0);

      // Verify no events were published
      const libraryEvents = eventPublisher.getPublishedEventsOfType(
        EventNames.CARD_ADDED_TO_LIBRARY,
      );
      expect(libraryEvents).toHaveLength(0);

      const collectionEvents = eventPublisher.getPublishedEventsOfType(
        EventNames.CARD_ADDED_TO_COLLECTION,
      );
      expect(collectionEvents).toHaveLength(0);
    });
  });
});
