import json
import logging

import pytest

from mcard.config.config_constants import (
    DEFAULT_PAGE_SIZE,
    DUPLICATE_EVENT_TYPE,
    HASH,
    TYPE,
)
from mcard.engine.sqlite_engine import SQLiteConnection, SQLiteEngine
from mcard.model.card import MCard
from mcard.model.card_collection import CardCollection
from mcard.model.g_time import GTime
from mcard.model.hash.validator import HashValidator

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)


@pytest.fixture
def sqlite_engine():
    conn = SQLiteConnection(":memory:")  # Use in-memory database for testing
    engine = SQLiteEngine(conn)
    yield engine
    engine.clear()


@pytest.fixture
def collection(sqlite_engine):
    return CardCollection(sqlite_engine)


def test_add_and_retrieve_card(collection):
    # Test adding a card
    card = MCard("Test content")
    hash_value = collection.add(card)
    logger.debug("Hash Value of this new card is: " + card.hash)

    # There are cases where the hash is not the same as the card's hash.
    if hash_value != card.hash:
        logger.debug("Hash Value returned by collection.add() operation: " + hash_value)

    # Retrieve the card using its hash
    retrieved_card = collection.get(
        card.hash
    )  # This must use card.hash as the input to the get method. This will ensure the retrieval gest to right card

    assert retrieved_card is not None  # Ensure the card was retrieved
    assert (
        retrieved_card.content == card.content
    )  # Ensure the retrieved card's content matches
    assert retrieved_card.hash == card.hash  # Ensure the retrieved card's hash matches


def test_delete_card(collection):
    # Add and then delete a card
    card = MCard("Test content")
    hash_value = collection.add(card)
    assert collection.delete(hash_value) is True
    assert collection.get(hash_value) is None


def test_pagination(collection):
    # Add multiple cards
    A_RANDOM_NUMBER_OF_CARDS = 15
    cards = [MCard(f"Content {i}") for i in range(A_RANDOM_NUMBER_OF_CARDS)]
    for card in cards:
        collection.add(card)

    # Test first page
    page = collection.get_page(page_number=1, page_size=DEFAULT_PAGE_SIZE)
    total_number_of_cards = page.total_items
    assert len(page.items) <= DEFAULT_PAGE_SIZE
    assert (
        page.total_items >= A_RANDOM_NUMBER_OF_CARDS
    )  # There should be more than A_RANDOM_NUMBER_OF_CARDS items, since if we turn off engine.clear() at the end of each test, we might have addition cards
    assert page.has_next is True
    assert page.has_previous is False

    # Test second page
    PAGE_NUMBER = 2
    page = collection.get_page(page_number=PAGE_NUMBER, page_size=DEFAULT_PAGE_SIZE)
    total_number_of_cards = page.total_items
    if total_number_of_cards > DEFAULT_PAGE_SIZE * PAGE_NUMBER:
        assert len(page.items) == DEFAULT_PAGE_SIZE
    else:
        assert len(page.items) == total_number_of_cards % DEFAULT_PAGE_SIZE

    if ((total_number_of_cards * 1.0 / DEFAULT_PAGE_SIZE) > PAGE_NUMBER) and (
        total_number_of_cards > DEFAULT_PAGE_SIZE * (PAGE_NUMBER - 1)
    ):
        assert page.has_next is True
    else:
        assert page.has_next is False
    if (total_number_of_cards > DEFAULT_PAGE_SIZE) and (PAGE_NUMBER > 1):
        assert page.has_previous is True


def test_clear_collection(collection):
    # Add some cards
    for i in range(5):
        collection.add(MCard(f"Content {i}"))

    # Get all cards directly to verify count
    all_cards = collection.get_all_mcards_raw()
    assert collection.count() == len(all_cards)

    collection.clear()
    assert collection.count() == 0


def test_duplicate_card_error(collection):
    DUPLICATE_STRING_VALUE = "Test content"
    # Add first card
    card1 = MCard(DUPLICATE_STRING_VALUE)

    locally_computed_hash = HashValidator.compute_hash(
        card1.content, GTime.get_hash_function(card1.g_time)
    )  # Ensure it returns the enum value
    assert card1.hash == locally_computed_hash
    first_hash = collection.add(card1)

    # Try to add another card with same content but different hash
    card2 = MCard(DUPLICATE_STRING_VALUE)  # Same content, will have different g_time
    assert first_hash == card2.hash

    # Add second card. The hash returned should be the ORIGINAL hash (idempotency).
    second_hash = collection.add(card2)

    assert first_hash == second_hash

    # Verify the original card is still there
    original_card = collection.get(first_hash)
    assert original_card.content == b"Test content"

    # Verify searching by content only finds the original (content match)
    # Note: search_by_content returns cards where content contains the string
    matching_cards = collection.search_by_content(b"Test content")
    # Should find at least the original card.
    assert any(c.hash == first_hash for c in matching_cards.items)

    # Verify duplicate event was created
    # Since add() returns the original hash, we must search for the event
    # The event contains the hash of the duplicated card
    results = collection.search_by_string(str(card2.hash))

    event_card = None
    for card in results.items:
        try:
            # Skip if it's the original card (unlikely to match hash string unless content has it)
            if card.hash == first_hash:
                continue

            data = json.loads(card.content)
            if data.get(TYPE) == DUPLICATE_EVENT_TYPE:
                event_card = card
                break
        except (json.JSONDecodeError, UnicodeDecodeError):
            continue

    assert event_card is not None, "Duplicate event card not found"

    # Parse and verify event data
    event_content = event_card.content
    assert event_content is not None

    assert isinstance(event_content, bytes)
    event_dict = json.loads(event_content)  # Convert to dictionary
    logger.debug(f"EVENT DICTIONARY: {event_dict}")
    assert event_dict[TYPE] == DUPLICATE_EVENT_TYPE
    assert event_dict[HASH] == str(card2.hash)  # Points to new card's hash
    assert GTime.is_iso_format(
        GTime.get_timestamp(event_dict["duplicate_time"])
    )  # Make sure timestamp is in ISO format


def test_invalid_pagination_parameters(collection):
    with pytest.raises(ValueError):
        collection.get_page(page_number=0)

    with pytest.raises(ValueError):
        collection.get_page(page_size=0)


def test_search_by_string(collection):
    # Add cards with different content and g_time
    cards = [
        MCard(content="Card 1"),
        MCard(content="Card 2"),
        MCard(content="Another Card"),
    ]
    for card in cards:
        collection.add(card)

    # Search for a term that exists in hash
    hash_value = str(cards[0].hash)  # Get the hash of the first card
    page = collection.search_by_string(hash_value)
    assert len(page.items) == 1
    assert (
        str(page.items[0].hash) == hash_value
    )  # Convert HashString to string for comparison

    # Allow for a larger margin of error in the g_time comparison
    actual_g_time = str(page.items[0].g_time)

    # Search for a term that exists in g_time
    partial_time_str = actual_g_time.split("|")[1][:8]
    logger.info(f"Partial time string: {partial_time_str}")
    page = collection.search_by_string(
        partial_time_str
    )  # Get the g_time of the first card
    assert len(page.items) >= len(cards)

    # Validate the g_time format
    # assert actual_g_time.startswith('md5' + '|')  # Check for hash function
    assert actual_g_time.endswith(
        f"|{GTime.get_region_code(actual_g_time)}"
    )  # Check for region code
    # Extract timestamp and validate
    timestamp_part = actual_g_time.split("|")[1]
    assert GTime.is_iso_format(timestamp_part)  # Validate timestamp format
    # Check for hash function
    hash_function = actual_g_time.split("|")[0]
    assert GTime.is_valid_hash_function(hash_function)
    # Check for three parts
    assert len(actual_g_time.split("|")) == 3  # Check for three parts

    # Search for a term that does not exist
    page = collection.search_by_string("Nonexistent")
    assert len(page.items) == 0


def test_search_by_content(collection):
    # Add test cards with different content
    test_contents = [
        b"Python is a great programming language",
        b"Testing is important in software development",
        b"Python has great testing tools like pytest",
        b"This is a test card with some content",
    ]

    # Add cards to collection
    for content in test_contents:
        collection.add(MCard(content))

    # Test search for content that should match one card
    results = collection.search_by_content(b"programming")
    assert len(results.items) == 1
    assert b"programming" in results.items[0].content

    # Test search for content that should match multiple cards
    results = collection.search_by_content(b"Python")
    assert len(results.items) == 2
    assert all(b"Python" in card.content for card in results.items)

    # Test search with no matches
    results = collection.search_by_content(b"nonexistentterm")
    assert len(results.items) == 0

    # Test pagination with case-sensitive search
    results = collection.search_by_content(b"test", page_size=2)
    assert len(results.items) == 2
    # Only 2 cards contain lowercase 'test' (case-sensitive search)
    assert results.total_items == 2
    assert results.page_number == 1
    assert results.page_size == 2
    assert results.total_pages == 1


def test_get_all_cards_empty_collection(collection):
    """Test get_all_cards with an empty collection."""
    cards, total = collection.get_all_cards()
    assert len(cards) == 0
    assert total == 0


def test_get_all_cards_single_page(collection):
    """Test get_all_cards with results that fit in a single page."""
    # Add test cards
    test_contents = [b"Card 1", b"Card 2", b"Card 3"]
    for content in test_contents:
        collection.add(MCard(content))

    # Get all cards with default page size (10)
    cards, total = collection.get_all_cards()

    # Verify results
    assert len(cards) == 3
    assert total == 3
    assert {card.content for card in cards} == set(test_contents)


def test_get_all_cards_multiple_pages(collection):
    """Test get_all_cards with results that span multiple pages."""
    # Add more cards than the default page size
    test_contents = [f"Card {i}".encode() for i in range(1, 15)]  # 14 cards
    for content in test_contents:
        collection.add(MCard(content))

    # Get all cards with a small page size to force pagination
    cards, total = collection.get_all_cards(page_size=5)

    # Verify results
    assert len(cards) == 14
    assert total == 14
    assert {card.content for card in cards} == set(test_contents)


def test_get_all_cards_with_callback(collection):
    """Test get_all_cards with a processing callback function."""
    # Add test cards
    test_contents = [b"Card 1", b"Card 2", b"Card 3"]
    for content in test_contents:
        collection.add(MCard(content))

    # Define a callback that transforms each card
    def process_card(card):
        return {"content": card.content.upper(), "hash": card.hash}

    # Get all cards with the processing callback
    processed_cards, total = collection.get_all_cards(process_callback=process_card)

    # Verify results
    assert len(processed_cards) == 3
    assert total == 3
    assert all(isinstance(card, dict) for card in processed_cards)
    assert all(card["content"] == card["content"].upper() for card in processed_cards)


def test_get_all_cards_with_large_page_size(collection):
    """Test get_all_cards with a page size larger than the number of cards."""
    # Add test cards
    test_contents = [b"Card 1", b"Card 2", b"Card 3"]
    for content in test_contents:
        collection.add(MCard(content))

    # Get all cards with a large page size
    cards, total = collection.get_all_cards(page_size=100)

    # Verify results
    assert len(cards) == 3
    assert total == 3
    assert {card.content for card in cards} == set(test_contents)


def test_get_all_cards_with_invalid_callback(collection):
    """Test get_all_cards with an invalid callback (should be ignored)."""
    # Add test cards
    test_contents = [b"Card 1", b"Card 2"]
    for content in test_contents:
        collection.add(MCard(content))

    # Get all cards with an invalid callback
    cards, total = collection.get_all_cards(process_callback="not a function")

    # Verify results (should still work, just without processing)
    assert len(cards) == 2
    assert total == 2
    assert all(isinstance(card, MCard) for card in cards)
