"""Tests for the SkillMemory — persistent per-skill key-value store."""

import json
import sqlite3
import tempfile
import threading
import time
from pathlib import Path

import pytest

from alive.memory.skill_memory import SkillMemory, display_memory, _format_timestamp


# ── Fixtures ────────────────────────────────────────────────────────────────


@pytest.fixture
def mem(tmp_path):
    """Create a SkillMemory with a temp database."""
    db = tmp_path / "test_skill" / "memory.db"
    m = SkillMemory("test_skill", db_path=db)
    yield m
    m.close()


@pytest.fixture
def populated_mem(tmp_path):
    """Create a SkillMemory with some data already stored."""
    db = tmp_path / "test_skill" / "memory.db"
    m = SkillMemory("test_skill", db_path=db)
    m.set("counter", 1)
    m.set("counter", 2)
    m.set("counter", 3)
    m.set("name", "alice")
    m.set("config", {"threshold": 0.8, "retries": 3})
    yield m
    m.close()


@pytest.fixture
def skills_dir(tmp_path):
    """Create a temp skills directory with a skill that has memory."""
    skill_dir = tmp_path / "tracker"
    skill_dir.mkdir()
    (skill_dir / "skill.py").write_text("def run(): return {}\n")
    (skill_dir / "metadata.json").write_text(json.dumps({
        "name": "tracker",
        "description": "Test skill",
        "times_used": 0,
        "success_count": 0,
        "fail_count": 0,
        "confidence_score": 0.5,
        "dependencies": [],
        "input_schema": {},
        "output_schema": {},
        "sample_input": {},
    }))

    # Create some memory
    mem = SkillMemory("tracker", skills_dir=tmp_path)
    mem.set("runs", 42)
    mem.set("last_status", "ok")
    mem.close()

    return tmp_path


# ── Core API tests ──────────────────────────────────────────────────────────


class TestGet:
    def test_get_missing_key_returns_default(self, mem):
        assert mem.get("nonexistent") is None

    def test_get_missing_key_custom_default(self, mem):
        assert mem.get("nonexistent", "fallback") == "fallback"

    def test_get_string_value(self, mem):
        mem.set("greeting", "hello")
        assert mem.get("greeting") == "hello"

    def test_get_int_value(self, mem):
        mem.set("count", 42)
        assert mem.get("count") == 42

    def test_get_float_value(self, mem):
        mem.set("pi", 3.14159)
        assert mem.get("pi") == 3.14159

    def test_get_bool_value(self, mem):
        mem.set("flag", True)
        assert mem.get("flag") is True

    def test_get_none_value(self, mem):
        mem.set("nothing", None)
        assert mem.get("nothing") is None

    def test_get_dict_value(self, mem):
        data = {"name": "alice", "age": 30, "tags": ["admin", "user"]}
        mem.set("profile", data)
        assert mem.get("profile") == data

    def test_get_list_value(self, mem):
        items = [1, "two", 3.0, None, True]
        mem.set("items", items)
        assert mem.get("items") == items

    def test_get_returns_latest_value(self, mem):
        mem.set("key", "first")
        mem.set("key", "second")
        mem.set("key", "third")
        assert mem.get("key") == "third"


class TestSet:
    def test_set_creates_db_file(self, tmp_path):
        db = tmp_path / "new_skill" / "memory.db"
        m = SkillMemory("new_skill", db_path=db)
        m.set("key", "value")
        assert db.exists()
        m.close()

    def test_set_appends_to_history(self, mem):
        mem.set("x", 1)
        mem.set("x", 2)
        mem.set("x", 3)
        history = mem.history("x")
        assert len(history) == 3
        # Newest first
        assert history[0][1] == 3
        assert history[1][1] == 2
        assert history[2][1] == 1

    def test_set_complex_nested_data(self, mem):
        data = {
            "results": [
                {"url": "https://example.com", "status": 200},
                {"url": "https://test.com", "status": 404},
            ],
            "metadata": {"total": 2, "cached": False},
        }
        mem.set("scan_results", data)
        assert mem.get("scan_results") == data

    def test_set_stores_timestamp(self, mem):
        mem.set("key", "value")
        history = mem.history("key")
        assert len(history) == 1
        ts = history[0][0]
        # Should be a valid ISO timestamp
        assert "T" in ts
        assert "+" in ts or "Z" in ts or ts.endswith("+00:00")


class TestDelete:
    def test_delete_existing_key(self, populated_mem):
        count = populated_mem.delete("counter")
        assert count == 3  # had 3 entries
        assert populated_mem.get("counter") is None

    def test_delete_nonexistent_key(self, mem):
        count = mem.delete("ghost")
        assert count == 0

    def test_delete_only_specified_key(self, populated_mem):
        populated_mem.delete("counter")
        # Other keys should remain
        assert populated_mem.get("name") == "alice"
        assert populated_mem.get("config") is not None


class TestListKeys:
    def test_empty_memory(self, mem):
        assert mem.list_keys() == []

    def test_returns_distinct_keys(self, populated_mem):
        keys = populated_mem.list_keys()
        assert sorted(keys) == ["config", "counter", "name"]

    def test_sorted_alphabetically(self, mem):
        mem.set("zebra", 1)
        mem.set("apple", 2)
        mem.set("mango", 3)
        assert mem.list_keys() == ["apple", "mango", "zebra"]


class TestClear:
    def test_clear_empty(self, mem):
        count = mem.clear()
        assert count == 0

    def test_clear_populated(self, populated_mem):
        count = populated_mem.clear()
        assert count == 5  # 3 counter + 1 name + 1 config
        assert populated_mem.list_keys() == []

    def test_clear_returns_entry_count(self, mem):
        mem.set("a", 1)
        mem.set("a", 2)
        mem.set("b", 3)
        count = mem.clear()
        assert count == 3


class TestHistory:
    def test_empty_history(self, mem):
        assert mem.history("nonexistent") == []

    def test_history_order_newest_first(self, populated_mem):
        history = populated_mem.history("counter")
        values = [v for _, v in history]
        assert values == [3, 2, 1]

    def test_history_limit(self, mem):
        for i in range(200):
            mem.set("counter", i)
        history = mem.history("counter", limit=50)
        assert len(history) == 50
        # Newest first
        assert history[0][1] == 199
        assert history[-1][1] == 150

    def test_history_default_limit(self, mem):
        for i in range(150):
            mem.set("key", i)
        history = mem.history("key")
        # Default limit is 100
        assert len(history) == 100

    def test_history_timestamps_are_valid(self, populated_mem):
        history = populated_mem.history("counter")
        for ts, _ in history:
            assert isinstance(ts, str)
            assert "T" in ts

    def test_history_returns_tuples(self, mem):
        mem.set("key", "value")
        history = mem.history("key")
        assert len(history) == 1
        assert isinstance(history[0], tuple)
        assert len(history[0]) == 2


class TestCount:
    def test_count_empty(self, mem):
        assert mem.count() == 0

    def test_count_all(self, populated_mem):
        assert populated_mem.count() == 5

    def test_count_by_key(self, populated_mem):
        assert populated_mem.count("counter") == 3
        assert populated_mem.count("name") == 1
        assert populated_mem.count("config") == 1

    def test_count_nonexistent_key(self, mem):
        assert mem.count("ghost") == 0


# ── Database behavior tests ─────────────────────────────────────────────────


class TestDatabaseBehavior:
    def test_db_path_property(self, mem, tmp_path):
        expected = tmp_path / "test_skill" / "memory.db"
        assert mem.db_path == expected

    def test_creates_parent_directories(self, tmp_path):
        db = tmp_path / "deep" / "nested" / "skill" / "memory.db"
        m = SkillMemory("skill", db_path=db)
        m.set("key", "value")
        assert db.exists()
        m.close()

    def test_persistence_across_instances(self, tmp_path):
        db = tmp_path / "persist_skill" / "memory.db"

        # Write with one instance
        m1 = SkillMemory("persist_skill", db_path=db)
        m1.set("key", "persisted_value")
        m1.close()

        # Read with a new instance
        m2 = SkillMemory("persist_skill", db_path=db)
        assert m2.get("key") == "persisted_value"
        m2.close()

    def test_close_and_reopen(self, tmp_path):
        db = tmp_path / "reopen_skill" / "memory.db"
        m = SkillMemory("reopen_skill", db_path=db)
        m.set("key", "value")
        m.close()

        # Reopen
        m2 = SkillMemory("reopen_skill", db_path=db)
        assert m2.get("key") == "value"
        m2.close()

    def test_repr(self, mem):
        r = repr(mem)
        assert "SkillMemory" in r
        assert "test_skill" in r

    def test_skills_dir_override(self, tmp_path):
        m = SkillMemory("my_skill", skills_dir=tmp_path)
        m.set("key", "value")
        expected_db = tmp_path / "my_skill" / "memory.db"
        assert expected_db.exists()
        assert m.get("key") == "value"
        m.close()


# ── Thread safety tests ─────────────────────────────────────────────────────


class TestThreadSafety:
    def test_concurrent_writes(self, tmp_path):
        db = tmp_path / "thread_skill" / "memory.db"
        m = SkillMemory("thread_skill", db_path=db)

        errors = []

        def writer(thread_id):
            try:
                for i in range(50):
                    m.set(f"thread_{thread_id}", i)
            except Exception as e:
                errors.append(str(e))

        threads = [threading.Thread(target=writer, args=(t,)) for t in range(4)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        m.close()
        assert errors == [], f"Thread errors: {errors}"

    def test_concurrent_reads_and_writes(self, tmp_path):
        db = tmp_path / "rw_skill" / "memory.db"
        m = SkillMemory("rw_skill", db_path=db)
        m.set("counter", 0)

        errors = []

        def writer():
            try:
                for i in range(50):
                    m.set("counter", i)
            except Exception as e:
                errors.append(str(e))

        def reader():
            try:
                for _ in range(50):
                    m.get("counter")
            except Exception as e:
                errors.append(str(e))

        threads = [
            threading.Thread(target=writer),
            threading.Thread(target=reader),
            threading.Thread(target=reader),
        ]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        m.close()
        assert errors == [], f"Thread errors: {errors}"


# ── display_memory tests ─────────────────────────────────────────────────────


class TestDisplayMemory:
    def test_display_empty(self, tmp_path):
        from rich.console import Console

        skill_dir = tmp_path / "empty_skill"
        skill_dir.mkdir()

        con = Console(file=open("/dev/null", "w"), force_terminal=False)
        display_memory("empty_skill", skills_dir=tmp_path, console=con)

    def test_display_populated(self, skills_dir):
        from rich.console import Console

        con = Console(file=open("/dev/null", "w"), force_terminal=False)
        display_memory("tracker", skills_dir=skills_dir, console=con)

    def test_display_with_complex_values(self, tmp_path):
        from rich.console import Console

        skill_dir = tmp_path / "complex_skill"
        skill_dir.mkdir()

        m = SkillMemory("complex_skill", skills_dir=tmp_path)
        m.set("data", {"nested": {"deep": [1, 2, 3]}, "flag": True})
        m.set("list", [1, 2, 3, 4, 5])
        m.set("string", "hello world")
        m.close()

        con = Console(file=open("/dev/null", "w"), force_terminal=False)
        display_memory("complex_skill", skills_dir=tmp_path, console=con)


# ── _format_timestamp tests ──────────────────────────────────────────────────


class TestFormatTimestamp:
    def test_valid_iso_timestamp(self):
        result = _format_timestamp("2026-03-15T10:30:00+00:00")
        assert "2026" in result
        assert "10:30" in result

    def test_invalid_timestamp_passthrough(self):
        assert _format_timestamp("not-a-date") == "not-a-date"

    def test_empty_string(self):
        assert _format_timestamp("") == ""


# ── Edge cases ───────────────────────────────────────────────────────────────


class TestEdgeCases:
    def test_empty_string_key(self, mem):
        mem.set("", "value")
        assert mem.get("") == "value"

    def test_unicode_key(self, mem):
        mem.set("price_tracker", 42)
        assert mem.get("price_tracker") == 42

    def test_large_value(self, mem):
        large_data = {"items": list(range(10000))}
        mem.set("big", large_data)
        assert mem.get("big") == large_data

    def test_special_characters_in_value(self, mem):
        data = {"sql": "SELECT * FROM table WHERE name = 'O''Brien'"}
        mem.set("query", data)
        assert mem.get("query") == data

    def test_overwrite_different_types(self, mem):
        mem.set("key", "string")
        mem.set("key", 42)
        mem.set("key", [1, 2, 3])
        mem.set("key", {"a": "b"})
        assert mem.get("key") == {"a": "b"}
        # History has all types
        history = mem.history("key")
        values = [v for _, v in history]
        assert values == [{"a": "b"}, [1, 2, 3], 42, "string"]
