import math
import uuid
import pytest
from datetime import datetime, timezone, timedelta
from unittest.mock import MagicMock, patch
from pulse.brain import reconsolidation
from pulse.core import event_bus

@pytest.fixture
def clean_labile():
    if reconsolidation.LABILE_ITEMS_FILE.exists():
        reconsolidation.LABILE_ITEMS_FILE.unlink()
    reconsolidation._labile_state.clear()
    event_bus._subscribers.clear()
    yield
    if reconsolidation.LABILE_ITEMS_FILE.exists():
        reconsolidation.LABILE_ITEMS_FILE.unlink()

def test_mark_labile(clean_labile):
    reconsolidation.mark_labile("test_id", "lessons")
    assert "test_id" in reconsolidation._labile_state
    assert reconsolidation._labile_state["test_id"]["table"] == "lessons"

def test_sweep_expired(clean_labile):
    # Manually add expired item
    past = (datetime.now(timezone.utc) - timedelta(seconds=60)).isoformat()
    reconsolidation._labile_state["expired_id"] = {"table": "lessons", "expires_at": past}
    
    # Manually add valid item
    future = (datetime.now(timezone.utc) + timedelta(seconds=60)).isoformat()
    reconsolidation._labile_state["valid_id"] = {"table": "lessons", "expires_at": future}
    
    reconsolidation._save_labile()
    
    reconsolidation._sweep_expired()
    assert "expired_id" not in reconsolidation._labile_state
    assert "valid_id" in reconsolidation._labile_state

def test_reinforce_fails_if_not_labile(clean_labile):
    assert not reconsolidation.reinforce("missing_id")


def test_lability_window_expires_after_30s(clean_labile):
    """Lability window must expire after TTL_SECONDS (30s): reinforce returns False."""
    assert reconsolidation.TTL_SECONDS == 30, (
        f"TTL must be 30 seconds, got {reconsolidation.TTL_SECONDS}"
    )
    item_id = "test_expiry_reinforce"
    reconsolidation.mark_labile(item_id, "lessons")
    assert item_id in reconsolidation._labile_state

    # Simulate expiry by backdating expires_at beyond the 30s window
    past = (datetime.now(timezone.utc) - timedelta(seconds=31)).isoformat()
    reconsolidation._labile_state[item_id]["expires_at"] = past
    reconsolidation._save_labile()

    result = reconsolidation.reinforce(item_id)
    assert result is False, "reinforce() must return False after 30s lability window expires"
    assert item_id not in reconsolidation._labile_state, (
        "Expired item must be evicted from labile state"
    )


def test_lability_window_expires_contradict(clean_labile):
    """contradict() must return False after the 30s lability window expires."""
    item_id = "test_expiry_contradict"
    reconsolidation.mark_labile(item_id, "lessons")
    assert item_id in reconsolidation._labile_state

    past = (datetime.now(timezone.utc) - timedelta(seconds=31)).isoformat()
    reconsolidation._labile_state[item_id]["expires_at"] = past
    reconsolidation._save_labile()

    result = reconsolidation.contradict(item_id)
    assert result is False, "contradict() must return False after 30s lability window expires"
    assert item_id not in reconsolidation._labile_state, (
        "Expired item must be evicted from labile state after contradict"
    )


def test_bcm_delta_diminishes_with_high_access_count(clean_labile):
    """reinforce() salience boost must be smaller for high-access items (BCM metaplasticity).

    Formula: bcm_delta = 0.3 / (1 + access_count/10)
    - ac=0   → delta ~0.300 (full boost)
    - ac=100 → delta ~0.027 (diminished)
    """
    from pulse.core.brain_db import get_conn

    conn = get_conn()
    now = datetime.now(timezone.utc).isoformat()
    low_id = f"test_bcm_low_{uuid.uuid4().hex[:8]}"
    high_id = f"test_bcm_high_{uuid.uuid4().hex[:8]}"
    base_salience = 3.0

    for item_id, ac in [(low_id, 0), (high_id, 100)]:
        conn.execute(
            "INSERT INTO lessons (id, fingerprint, content, domain, salience, access_count, timestamp) "
            "VALUES (?, ?, ?, ?, ?, ?, ?)",
            (item_id, f"fp_{item_id}", f"test content {item_id}", "test_bcm_domain_xyz",
             base_salience, ac, now),
        )
    conn.commit()

    try:
        reconsolidation.mark_labile(low_id, "lessons")
        reconsolidation.mark_labile(high_id, "lessons")

        assert reconsolidation.reinforce(low_id), "low-access item should reinforce successfully"
        assert reconsolidation.reinforce(high_id), "high-access item should reinforce successfully"

        low_sal = conn.execute("SELECT salience FROM lessons WHERE id = ?", (low_id,)).fetchone()["salience"]
        high_sal = conn.execute("SELECT salience FROM lessons WHERE id = ?", (high_id,)).fetchone()["salience"]

        low_delta = round(low_sal - base_salience, 3)
        high_delta = round(high_sal - base_salience, 3)

        assert low_delta > high_delta, (
            f"BCM metaplasticity: low-access delta ({low_delta}) must exceed "
            f"high-access delta ({high_delta})"
        )
        assert low_delta > 0.25, f"Fresh item (ac=0) should get ~0.300 boost, got {low_delta}"
        assert high_delta < 0.05, f"High-access item (ac=100) should get ~0.027 boost, got {high_delta}"
    finally:
        conn.execute("DELETE FROM lessons WHERE id IN (?, ?)", (low_id, high_id))
        conn.commit()


# ---------------------------------------------------------------------------
# Critical Periods Plasticity — Architecture Domain Audit
# ---------------------------------------------------------------------------

def _mock_conn_returning_timestamp(ts_iso: str):
    """Mock DB connection whose execute().fetchone() returns the given timestamp."""
    row = MagicMock()
    row.__getitem__ = lambda self, key: ts_iso if key == "first_ts" else None
    mock_cursor = MagicMock()
    mock_cursor.fetchone.return_value = row
    mock_conn = MagicMock()
    mock_conn.execute.return_value = mock_cursor
    return mock_conn


def _plasticity_for_age(age_days: float) -> float:
    M = reconsolidation.CRITICAL_PERIOD_MIDPOINT
    S = reconsolidation.CRITICAL_PERIOD_STEEPNESS
    raw = 1.0 / (1.0 + math.exp(S * (age_days - M)))
    return max(reconsolidation.MIN_PLASTICITY, round(raw, 3))


def test_architecture_domain_plasticity_at_24_days():
    """Architecture domain (~24 days old at PULSE launch) must be highly plastic (>0.85)."""
    age_days = 24
    ts = (datetime.now(timezone.utc) - timedelta(days=age_days)).isoformat()
    mock_conn = _mock_conn_returning_timestamp(ts)
    with patch("pulse.brain.reconsolidation.get_conn", return_value=mock_conn):
        p = reconsolidation.get_domain_plasticity("architecture")
    expected = _plasticity_for_age(age_days)
    assert p == expected, f"Expected {expected} for 24-day architecture domain, got {p}"
    assert p > 0.85, f"Architecture at 24 days must be highly plastic (>0.85), got {p}"


def test_architecture_domain_plasticity_decreases_monotonically():
    """Plasticity decreases monotonically as architecture domain matures."""
    ages = [0, 30, 60, 90, 120, 180]
    plasticities = []
    for age in ages:
        ts = (datetime.now(timezone.utc) - timedelta(days=age)).isoformat()
        mock_conn = _mock_conn_returning_timestamp(ts)
        with patch("pulse.brain.reconsolidation.get_conn", return_value=mock_conn):
            p = reconsolidation.get_domain_plasticity("architecture")
        plasticities.append(p)
    for i in range(len(plasticities) - 1):
        assert plasticities[i] >= plasticities[i + 1], (
            f"Plasticity must decrease: age {ages[i]}d={plasticities[i]} "
            f">= age {ages[i + 1]}d={plasticities[i + 1]}"
        )


def test_architecture_domain_plasticity_at_midpoint():
    """Architecture domain at 90 days (sigmoid midpoint) must have plasticity ~0.5."""
    ts = (datetime.now(timezone.utc) - timedelta(days=90)).isoformat()
    mock_conn = _mock_conn_returning_timestamp(ts)
    with patch("pulse.brain.reconsolidation.get_conn", return_value=mock_conn):
        p = reconsolidation.get_domain_plasticity("architecture")
    assert abs(p - 0.5) < 0.01, f"Midpoint plasticity must be ~0.5, got {p}"


def test_mature_architecture_domain_respects_floor():
    """Architecture domain at 365 days must not drop below MIN_PLASTICITY floor."""
    ts = (datetime.now(timezone.utc) - timedelta(days=365)).isoformat()
    mock_conn = _mock_conn_returning_timestamp(ts)
    with patch("pulse.brain.reconsolidation.get_conn", return_value=mock_conn):
        p = reconsolidation.get_domain_plasticity("architecture")
    assert p >= reconsolidation.MIN_PLASTICITY, (
        f"Plasticity {p} fell below MIN_PLASTICITY={reconsolidation.MIN_PLASTICITY}"
    )
    assert p == reconsolidation.MIN_PLASTICITY, (
        f"Fully mature architecture domain must be exactly at floor={reconsolidation.MIN_PLASTICITY}, got {p}"
    )


def test_db_error_returns_fully_plastic():
    """DB failure in get_domain_plasticity must default to 1.0 (safe/fully plastic)."""
    mock_conn = MagicMock()
    mock_conn.execute.side_effect = Exception("DB unavailable")
    with patch("pulse.brain.reconsolidation.get_conn", return_value=mock_conn):
        p = reconsolidation.get_domain_plasticity("architecture")
    assert p == 1.0, f"DB error must default to 1.0 (fully plastic), got {p}"


def test_promotion_threshold_is_fixed_not_plasticity_adaptive():
    """Audit finding: stage_promote_to_patterns uses a fixed conf >= 0.7 threshold
    regardless of domain plasticity. Young architecture items (conf 0.5-0.69) are
    blocked from promotion despite high plasticity — anti-pattern: fixed thresholds
    not adaptive to domain maturity.
    """
    import inspect
    from pulse.daemons.consolidation.stages.stage_promotion import stage_promote_to_patterns
    src = inspect.getsource(stage_promote_to_patterns)
    assert "0.7" in src, "Fixed threshold 0.7 must exist — documents the anti-pattern"
    assert "get_domain_plasticity" not in src, (
        "stage_promote_to_patterns does NOT use get_domain_plasticity — "
        "promotion threshold is fixed, not adaptive to domain maturity (known anti-pattern)"
    )


# ---------------------------------------------------------------------------
# Event Bus Publishing — Memory Reconsolidation Events
# ---------------------------------------------------------------------------

def test_reinforce_publishes_event_on_bus(clean_labile):
    """reinforce() must publish memory.reconsolidation.reinforce to the event bus."""
    from pulse.core.enums import EventChannel

    item_id = "test_reinforce_event_id"
    future = (datetime.now(timezone.utc) + timedelta(seconds=60)).isoformat()
    reconsolidation._labile_state[item_id] = {"table": "lessons", "expires_at": future}

    row = MagicMock()
    row.__getitem__ = lambda self, key: {"access_count": 0, "salience": 3.0, "domain": "test"}.get(key)
    mock_cursor = MagicMock()
    mock_cursor.fetchone.return_value = row
    mock_conn = MagicMock()
    mock_conn.execute.return_value = mock_cursor

    received = []
    event_bus.subscribe(EventChannel.MEMORY_REINFORCE, received.append)

    with patch("pulse.brain.reconsolidation.get_conn", return_value=mock_conn), \
         patch("pulse.brain.reconsolidation.get_domain_plasticity", return_value=1.0):
        result = reconsolidation.reinforce(item_id)

    assert result is True
    assert len(received) == 1
    assert received[0]["type"] == EventChannel.MEMORY_REINFORCE
    assert received[0]["source"] == "reconsolidation"
    assert received[0]["payload"]["item_id"] == item_id


def test_contradict_publishes_event_on_bus(clean_labile):
    """contradict() must publish memory.reconsolidation.contradict to the event bus."""
    from pulse.core.enums import EventChannel

    item_id = "test_contradict_event_id"
    future = (datetime.now(timezone.utc) + timedelta(seconds=60)).isoformat()
    reconsolidation._labile_state[item_id] = {"table": "lessons", "expires_at": future}

    row = MagicMock()
    row.__getitem__ = lambda self, key: {"domain": "test"}.get(key)
    mock_cursor = MagicMock()
    mock_cursor.fetchone.return_value = row
    mock_conn = MagicMock()
    mock_conn.execute.return_value = mock_cursor

    received = []
    event_bus.subscribe(EventChannel.MEMORY_CONTRADICT, received.append)

    with patch("pulse.brain.reconsolidation.get_conn", return_value=mock_conn), \
         patch("pulse.brain.reconsolidation.get_domain_plasticity", return_value=0.8):
        result = reconsolidation.contradict(item_id)

    assert result is True
    assert len(received) == 1
    assert received[0]["type"] == EventChannel.MEMORY_CONTRADICT
    assert received[0]["source"] == "reconsolidation"
    assert received[0]["payload"]["item_id"] == item_id
    assert received[0]["payload"]["plasticity"] == 0.8


def test_reconsolidation_event_channels_match_enums():
    """Event channel strings in reconsolidation.py must match EventChannel constants."""
    import inspect
    from pulse.core.enums import EventChannel

    src = inspect.getsource(reconsolidation)
    assert EventChannel.MEMORY_REINFORCE in src, (
        f"reconsolidation.py must publish '{EventChannel.MEMORY_REINFORCE}' — not found in source"
    )
    assert EventChannel.MEMORY_CONTRADICT in src, (
        f"reconsolidation.py must publish '{EventChannel.MEMORY_CONTRADICT}' — not found in source"
    )


# ---------------------------------------------------------------------------
# Contradict → contested validity at confidence=0 (real DB)
# ---------------------------------------------------------------------------

def test_contradict_marks_contested_when_confidence_reaches_zero(clean_labile):
    """contradict() must set validity='contested' when confidence drops to 0.

    Scenario: confidence=0.1, plasticity=1.0 → delta=0.2 → confidence floors at 0.0,
    CASE condition (confidence - delta <= 0.0) is True → validity must be 'contested'.
    """
    from pulse.core.brain_db import get_conn
    from unittest.mock import patch

    conn = get_conn()
    now = datetime.now(timezone.utc).isoformat()
    item_id = f"test_contested_{uuid.uuid4().hex[:8]}"

    conn.execute(
        "INSERT INTO lessons (id, fingerprint, content, domain, confidence, salience, access_count, timestamp) "
        "VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
        (item_id, f"fp_{item_id}", "test contested content", "test_domain", 0.1, 3.0, 0, now),
    )
    conn.commit()

    try:
        future = (datetime.now(timezone.utc) + timedelta(seconds=60)).isoformat()
        reconsolidation._labile_state[item_id] = {"table": "lessons", "expires_at": future}

        with patch("pulse.brain.reconsolidation.get_domain_plasticity", return_value=1.0):
            result = reconsolidation.contradict(item_id)

        assert result is True, "contradict() must return True for a labile item"
        row = conn.execute(
            "SELECT confidence, validity FROM lessons WHERE id = ?", (item_id,)
        ).fetchone()
        assert row["confidence"] == 0.0, (
            f"confidence must be floored at 0.0 after contradict, got {row['confidence']}"
        )
        assert row["validity"] == "contested", (
            f"validity must be 'contested' when confidence reaches 0.0, got '{row['validity']}'"
        )
    finally:
        conn.execute("DELETE FROM lessons WHERE id = ?", (item_id,))
        conn.commit()


def test_contradict_does_not_mark_contested_when_confidence_stays_above_zero(clean_labile):
    """contradict() must NOT set validity='contested' when confidence stays above 0.

    Scenario: confidence=0.5, plasticity=1.0 → delta=0.2 → confidence=0.3,
    validity must remain 'valid'.
    """
    from pulse.core.brain_db import get_conn
    from unittest.mock import patch

    conn = get_conn()
    now = datetime.now(timezone.utc).isoformat()
    item_id = f"test_not_contested_{uuid.uuid4().hex[:8]}"

    conn.execute(
        "INSERT INTO lessons (id, fingerprint, content, domain, confidence, salience, access_count, timestamp) "
        "VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
        (item_id, f"fp_{item_id}", "test not contested content", "test_domain", 0.5, 3.0, 0, now),
    )
    conn.commit()

    try:
        future = (datetime.now(timezone.utc) + timedelta(seconds=60)).isoformat()
        reconsolidation._labile_state[item_id] = {"table": "lessons", "expires_at": future}

        with patch("pulse.brain.reconsolidation.get_domain_plasticity", return_value=1.0):
            result = reconsolidation.contradict(item_id)

        assert result is True, "contradict() must return True for a labile item"
        row = conn.execute(
            "SELECT confidence, validity FROM lessons WHERE id = ?", (item_id,)
        ).fetchone()
        assert row["confidence"] == pytest.approx(0.3, abs=1e-6), (
            f"confidence must be 0.5 - 0.2 = 0.3, got {row['confidence']}"
        )
        assert row["validity"] == "valid", (
            f"validity must remain 'valid' when confidence stays above 0.0, got '{row['validity']}'"
        )
    finally:
        conn.execute("DELETE FROM lessons WHERE id = ?", (item_id,))
        conn.commit()