"""
Tests for Story 9.03 — RedisEpochLock
File: tests/track_b/test_story_9_03.py

BB Tests: BB1, BB2, BB3, BB4
WB Tests: WB1, WB2, WB3, WB4
"""

import os
import sys
import socket
from unittest.mock import MagicMock, call

import pytest

sys.path.insert(0, "/mnt/e/genesis-system")

from core.epoch.redis_epoch_lock import (
    RedisEpochLock,
    EPOCH_LOCK_KEY,
    EPOCH_LOCK_TTL,
)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def make_mock_redis():
    """Return a fresh MagicMock that models an empty Redis store."""
    r = MagicMock()
    # Default: key does not exist
    r.get.return_value = None
    # Default: SET NX succeeds (returns True)
    r.set.return_value = True
    return r


# ---------------------------------------------------------------------------
# BB Tests — Black-Box Behaviour
# ---------------------------------------------------------------------------

class TestBB1_FirstAcquireSucceedsSecondFails:
    """BB1: First acquire returns True; second acquire (different epoch) returns False."""

    def test_first_acquire_returns_true(self):
        r = make_mock_redis()
        lock = RedisEpochLock(r)
        assert lock.acquire("e1") is True

    def test_second_acquire_returns_false_when_held(self):
        r = make_mock_redis()
        # Simulate lock already held: first SET NX returns True, second returns None/False
        r.set.side_effect = [True, None]
        lock = RedisEpochLock(r)
        assert lock.acquire("e1") is True
        assert lock.acquire("e2") is False

    def test_second_acquire_with_different_instance_returns_false(self):
        """Simulates two separate instances competing for the lock."""
        r = make_mock_redis()
        r.set.side_effect = [True, None]  # second instance gets None (already held)
        lock1 = RedisEpochLock(r)
        lock2 = RedisEpochLock(r)
        assert lock1.acquire("e1") is True
        assert lock2.acquire("e2") is False


class TestBB2_ReleaseAllowsReacquire:
    """BB2: release() frees the lock so a subsequent acquire() succeeds."""

    def test_release_then_reacquire(self):
        r = make_mock_redis()
        epoch_id = "e1"
        hostname = socket.gethostname()
        pid = os.getpid()
        lock_value = f"{epoch_id}:{hostname}:{pid}".encode("utf-8")

        # First acquire succeeds
        r.set.return_value = True
        lock = RedisEpochLock(r)
        assert lock.acquire(epoch_id) is True

        # Release: get returns our lock value
        r.get.return_value = lock_value
        lock.release(epoch_id)
        r.delete.assert_called_once_with(EPOCH_LOCK_KEY)

        # After release, lock is free — next acquire succeeds
        r.set.return_value = True
        r.set.reset_mock()
        lock2 = RedisEpochLock(r)
        assert lock2.acquire("e3") is True

    def test_release_clears_held_value(self):
        r = make_mock_redis()
        epoch_id = "e1"
        lock_value = f"{epoch_id}:{socket.gethostname()}:{os.getpid()}".encode()
        r.set.return_value = True
        lock = RedisEpochLock(r)
        lock.acquire(epoch_id)
        assert lock._held_value is not None

        r.get.return_value = lock_value
        lock.release(epoch_id)
        assert lock._held_value is None


class TestBB3_GetLockHolder:
    """BB3: get_lock_holder() returns value while held, None when released."""

    def test_returns_lock_value_while_held(self):
        r = make_mock_redis()
        r.set.return_value = True
        lock = RedisEpochLock(r)
        lock.acquire("e1")

        expected = f"e1:{socket.gethostname()}:{os.getpid()}"
        r.get.return_value = expected.encode("utf-8")

        holder = lock.get_lock_holder()
        assert holder == expected

    def test_returns_none_when_not_held(self):
        r = make_mock_redis()
        r.get.return_value = None
        lock = RedisEpochLock(r)
        assert lock.get_lock_holder() is None

    def test_returns_string_not_bytes(self):
        r = make_mock_redis()
        r.set.return_value = True
        r.get.return_value = b"e1:myhost:1234"
        lock = RedisEpochLock(r)
        lock.acquire("e1")
        holder = lock.get_lock_holder()
        assert isinstance(holder, str)
        assert holder == "e1:myhost:1234"


class TestBB4_NoRedisClient:
    """BB4: When redis_client is None, acquire always returns True, release is a no-op."""

    def test_acquire_without_redis_returns_true(self):
        lock = RedisEpochLock(redis_client=None)
        assert lock.acquire("e1") is True

    def test_acquire_always_true_without_redis(self):
        """Multiple acquires all return True with no Redis."""
        lock = RedisEpochLock(redis_client=None)
        assert lock.acquire("e1") is True
        assert lock.acquire("e2") is True
        assert lock.acquire("e3") is True

    def test_release_without_redis_is_noop(self):
        lock = RedisEpochLock(redis_client=None)
        lock.acquire("e1")
        # Should not raise
        lock.release("e1")

    def test_get_lock_holder_without_redis_returns_none(self):
        lock = RedisEpochLock(redis_client=None)
        lock.acquire("e1")
        assert lock.get_lock_holder() is None


# ---------------------------------------------------------------------------
# WB Tests — White-Box / Internal Implementation
# ---------------------------------------------------------------------------

class TestWB1_AtomicSetNxEx:
    """WB1: acquire() calls redis.set with nx=True and ex=EPOCH_LOCK_TTL."""

    def test_set_called_with_nx_and_ex(self):
        r = make_mock_redis()
        r.set.return_value = True
        lock = RedisEpochLock(r)
        lock.acquire("e1")

        assert r.set.called
        _, kwargs = r.set.call_args
        assert kwargs.get("nx") is True, "nx=True required for atomic set-if-not-exists"
        assert kwargs.get("ex") == EPOCH_LOCK_TTL, f"ex must be {EPOCH_LOCK_TTL}"

    def test_set_called_with_correct_key(self):
        r = make_mock_redis()
        r.set.return_value = True
        lock = RedisEpochLock(r)
        lock.acquire("e1")

        args, _ = r.set.call_args
        assert args[0] == EPOCH_LOCK_KEY

    def test_lock_value_contains_epoch_id(self):
        r = make_mock_redis()
        r.set.return_value = True
        lock = RedisEpochLock(r)
        lock.acquire("epoch_20260225")

        args, _ = r.set.call_args
        lock_value = args[1]
        assert lock_value.startswith("epoch_20260225:")


class TestWB2_LockValueFormat:
    """WB2: Lock value contains epoch_id as prefix (used by release startswith check)."""

    def test_lock_value_starts_with_epoch_id(self):
        r = make_mock_redis()
        r.set.return_value = True
        lock = RedisEpochLock(r)
        lock.acquire("myepoch")

        args, _ = r.set.call_args
        value = args[1]
        assert value.startswith("myepoch:")

    def test_lock_value_contains_hostname(self):
        r = make_mock_redis()
        r.set.return_value = True
        lock = RedisEpochLock(r)
        lock.acquire("e1")

        args, _ = r.set.call_args
        value = args[1]
        assert socket.gethostname() in value

    def test_lock_value_contains_pid(self):
        r = make_mock_redis()
        r.set.return_value = True
        lock = RedisEpochLock(r)
        lock.acquire("e1")

        args, _ = r.set.call_args
        value = args[1]
        assert str(os.getpid()) in value


class TestWB3_ReleaseOwnershipCheck:
    """WB3: release() only deletes if lock value starts with epoch_id."""

    def test_release_deletes_when_epoch_id_matches(self):
        r = make_mock_redis()
        r.set.return_value = True
        r.get.return_value = b"e1:myhost:9999"
        lock = RedisEpochLock(r)
        lock.acquire("e1")
        lock.release("e1")
        r.delete.assert_called_once_with(EPOCH_LOCK_KEY)

    def test_release_does_not_delete_wrong_epoch_id(self):
        r = make_mock_redis()
        r.set.return_value = True
        # Lock is held by "e1", but we try to release with "e2"
        r.get.return_value = b"e1:myhost:9999"
        lock = RedisEpochLock(r)
        lock.acquire("e1")
        lock.release("e2")  # Wrong epoch_id
        r.delete.assert_not_called()

    def test_release_noop_when_key_not_found(self):
        r = make_mock_redis()
        r.set.return_value = True
        r.get.return_value = None  # Key already gone
        lock = RedisEpochLock(r)
        lock.acquire("e1")
        lock.release("e1")
        r.delete.assert_not_called()


class TestWB4_Constants:
    """WB4: EPOCH_LOCK_KEY and EPOCH_LOCK_TTL have correct values."""

    def test_epoch_lock_key_value(self):
        assert EPOCH_LOCK_KEY == "epoch:lock:nightly"

    def test_epoch_lock_ttl_value(self):
        assert EPOCH_LOCK_TTL == 7200

    def test_epoch_lock_ttl_is_two_hours(self):
        assert EPOCH_LOCK_TTL == 2 * 3600


# ---------------------------------------------------------------------------
# Context Manager Tests
# ---------------------------------------------------------------------------

class TestContextManager:
    """Context manager: with lock.for_epoch("e1") as (lk, acquired)."""

    def test_context_manager_acquires_on_enter(self):
        r = make_mock_redis()
        r.set.return_value = True
        lock = RedisEpochLock(r)
        with lock.for_epoch("e1") as (lk, acquired):
            assert acquired is True
            assert lk is lock

    def test_context_manager_releases_on_exit(self):
        r = make_mock_redis()
        r.set.return_value = True
        lock = RedisEpochLock(r)
        lock_value = f"e1:{socket.gethostname()}:{os.getpid()}".encode()
        r.get.return_value = lock_value
        with lock.for_epoch("e1") as (lk, acquired):
            pass
        r.delete.assert_called_once_with(EPOCH_LOCK_KEY)

    def test_context_manager_releases_on_exception(self):
        r = make_mock_redis()
        r.set.return_value = True
        lock = RedisEpochLock(r)
        lock_value = f"e1:{socket.gethostname()}:{os.getpid()}".encode()
        r.get.return_value = lock_value
        with pytest.raises(ValueError):
            with lock.for_epoch("e1") as (lk, acquired):
                raise ValueError("test error")
        r.delete.assert_called_once_with(EPOCH_LOCK_KEY)

    def test_context_manager_acquired_false_when_lock_held(self):
        r = make_mock_redis()
        r.set.return_value = None  # Already held
        lock = RedisEpochLock(r)
        with lock.for_epoch("e2") as (lk, acquired):
            assert acquired is False

    def test_context_manager_no_redis(self):
        lock = RedisEpochLock(redis_client=None)
        with lock.for_epoch("e1") as (lk, acquired):
            assert acquired is True


# ---------------------------------------------------------------------------
# Import Tests
# ---------------------------------------------------------------------------

class TestImports:
    """Verify __init__.py exports are correct."""

    def test_import_from_package(self):
        from core.epoch import RedisEpochLock, EPOCH_LOCK_KEY, EPOCH_LOCK_TTL
        assert RedisEpochLock is not None
        assert EPOCH_LOCK_KEY == "epoch:lock:nightly"
        assert EPOCH_LOCK_TTL == 7200

    def test_direct_module_import(self):
        from core.epoch.redis_epoch_lock import RedisEpochLock
        lock = RedisEpochLock(redis_client=None)
        assert lock.acquire("test") is True
