"""
Tests for Phase 47: Data Export & Portability — GDPR/Privacy Compliance.
"""

import json
from pathlib import Path

import pytest


@pytest.fixture(autouse=True)
def _isolate(tmp_path, monkeypatch):
    """Redirect all storage to tmp_path so tests are isolated."""
    # Create required subdirectories
    for subdir in (
        "channels",
        "sessions",
        "cursors",
        "profiles",
        "tasks",
        "dms",
        "dms/cursors",
        "dms/blocks",
        "memory",
        "audit",
        "notification_prefs",
        "sandboxes",
        "exports",
        "exports/backups",
    ):
        (tmp_path / subdir).mkdir(parents=True, exist_ok=True)

    import brynq.server as srv
    monkeypatch.setattr(srv, "BROKER_DIR", tmp_path)
    monkeypatch.setattr(srv, "CHANNELS_DIR", tmp_path / "channels")
    monkeypatch.setattr(srv, "SESSIONS_DIR", tmp_path / "sessions")

    import brynq.data_export as de
    monkeypatch.setattr(de, "BROKER_DIR", tmp_path)
    monkeypatch.setattr(de, "CHANNELS_DIR", tmp_path / "channels")
    monkeypatch.setattr(de, "SESSIONS_DIR", tmp_path / "sessions")
    monkeypatch.setattr(de, "EXPORTS_DIR", tmp_path / "exports")
    monkeypatch.setattr(de, "REQUESTS_PATH", tmp_path / "exports" / "requests.jsonl")
    monkeypatch.setattr(de, "BACKUPS_DIR", tmp_path / "exports" / "backups")
    monkeypatch.setattr(de, "EXPORT_AUDIT_PATH", tmp_path / "exports" / "audit.jsonl")


# ── Helpers ───────────────────────────────────────────────────────────────


def _seed_profile(tmp_path, user_id, extra=None):
    profile = {
        "session_id": user_id,
        "session_name": f"bot-{user_id}",
        "bio": "Test bot",
        "skills": ["python"],
        "reputation": 4.5,
        "tasks_completed": 10,
        "email": f"{user_id}@test.com",
    }
    if extra:
        profile.update(extra)
    path = tmp_path / "profiles" / f"{user_id}.json"
    path.write_text(json.dumps(profile), encoding="utf-8")
    return profile


def _seed_task(tmp_path, task_id, created_by, assigned_to=None):
    task = {
        "task_id": task_id,
        "created_by": created_by,
        "assigned_to": assigned_to,
        "title": f"Task {task_id}",
        "status": "pending",
    }
    path = tmp_path / "tasks" / f"{task_id}.json"
    path.write_text(json.dumps(task), encoding="utf-8")
    return task


def _seed_channel_msg(tmp_path, channel, session_id, message):
    entry = {
        "id": "msg001",
        "session_id": session_id,
        "session_name": f"bot-{session_id}",
        "channel": channel,
        "message": message,
    }
    path = tmp_path / "channels" / f"{channel}.jsonl"
    with path.open("a", encoding="utf-8") as f:
        f.write(json.dumps(entry) + "\n")
    return entry


def _seed_dm(tmp_path, conv_key, sender_id, message):
    entry = {
        "id": "dm001",
        "sender_id": sender_id,
        "sender_name": f"bot-{sender_id}",
        "message": message,
    }
    path = tmp_path / "dms" / f"{conv_key}.jsonl"
    with path.open("a", encoding="utf-8") as f:
        f.write(json.dumps(entry) + "\n")
    return entry


def _seed_rating(tmp_path, rated_id, rater_id, score):
    entry = {
        "rated_id": rated_id,
        "rater_id": rater_id,
        "score": score,
    }
    path = tmp_path / "profiles" / "_ratings.jsonl"
    with path.open("a", encoding="utf-8") as f:
        f.write(json.dumps(entry) + "\n")
    return entry


def _seed_memory(tmp_path, user_id, key, value):
    mem_dir = tmp_path / "memory" / user_id
    mem_dir.mkdir(parents=True, exist_ok=True)
    entry = {"key": key, "value": value, "session_id": user_id}
    (mem_dir / f"{key}.json").write_text(json.dumps(entry), encoding="utf-8")
    return entry


def _seed_prefs(tmp_path, user_id):
    prefs = {"agent_id": user_id, "dnd": {"enabled": False}}
    path = tmp_path / "notification_prefs" / f"{user_id}.json"
    path.write_text(json.dumps(prefs), encoding="utf-8")
    return prefs


def _seed_sandbox(tmp_path, user_id):
    sandbox = {"agent_id": user_id, "trust_level": "contributor"}
    path = tmp_path / "sandboxes" / f"{user_id}.json"
    path.write_text(json.dumps(sandbox), encoding="utf-8")
    return sandbox


def _seed_audit(tmp_path, actor_id, action="message_sent"):
    entry = {
        "actor_id": actor_id,
        "action": action,
        "timestamp": "2026-01-01T00:00:00+00:00",
    }
    path = tmp_path / "audit" / "trail.jsonl"
    with path.open("a", encoding="utf-8") as f:
        f.write(json.dumps(entry) + "\n")
    return entry


# ── 1. Export User Data ──────────────────────────────────────────────────


class TestExportUserData:
    def test_export_empty_user(self, tmp_path):
        from brynq.data_export import export_user_data
        data = export_user_data("ghost")
        assert "export_metadata" in data
        assert data["export_metadata"]["user_id"] == "ghost"
        assert data["profile"] == {}

    def test_export_with_profile(self, tmp_path):
        from brynq.data_export import export_user_data
        _seed_profile(tmp_path, "alice")
        data = export_user_data("alice")
        assert data["profile"]["session_id"] == "alice"
        assert data["profile"]["bio"] == "Test bot"

    def test_export_collects_tasks(self, tmp_path):
        from brynq.data_export import export_user_data
        _seed_task(tmp_path, "t1", created_by="bob")
        _seed_task(tmp_path, "t2", created_by="other", assigned_to="bob")
        data = export_user_data("bob")
        assert len(data["tasks"]) == 2

    def test_export_collects_messages(self, tmp_path):
        from brynq.data_export import export_user_data
        _seed_channel_msg(tmp_path, "general", "carol", "hello")
        _seed_channel_msg(tmp_path, "general", "other", "bye")
        data = export_user_data("carol")
        assert len(data["messages"]) == 1
        assert data["messages"][0]["message"] == "hello"

    def test_export_collects_dms(self, tmp_path):
        from brynq.data_export import export_user_data
        _seed_dm(tmp_path, "conv-abc", "dave", "hi dm")
        data = export_user_data("dave")
        assert len(data["dms"]) == 1

    def test_export_collects_reputation(self, tmp_path):
        from brynq.data_export import export_user_data
        _seed_rating(tmp_path, "eve", "frank", 5)
        data = export_user_data("eve")
        assert len(data["reputation"]) == 1

    def test_export_collects_memory(self, tmp_path):
        from brynq.data_export import export_user_data
        _seed_memory(tmp_path, "grace", "project", "web-app")
        data = export_user_data("grace")
        assert len(data["memory"]) == 1

    def test_export_requires_user_id(self):
        from brynq.data_export import export_user_data
        with pytest.raises(ValueError, match="user_id is required"):
            export_user_data("")

    def test_export_includes_all_sections(self, tmp_path):
        from brynq.data_export import export_user_data, ALL_SECTIONS
        data = export_user_data("test-all")
        for section in ALL_SECTIONS:
            assert section in data


# ── 2. Export Formats ────────────────────────────────────────────────────


class TestExportFormats:
    def test_export_as_json_returns_string(self, tmp_path):
        from brynq.data_export import export_as_json
        _seed_profile(tmp_path, "json-user")
        result = export_as_json("json-user")
        assert isinstance(result, str)
        parsed = json.loads(result)
        assert parsed["profile"]["session_id"] == "json-user"

    def test_export_as_csv_returns_dict(self, tmp_path):
        from brynq.data_export import export_as_csv
        _seed_profile(tmp_path, "csv-user")
        _seed_task(tmp_path, "ct1", created_by="csv-user")
        result = export_as_csv("csv-user")
        assert isinstance(result, dict)
        assert "profile" in result
        assert "tasks" in result

    def test_csv_profile_has_key_value_rows(self, tmp_path):
        from brynq.data_export import export_as_csv
        _seed_profile(tmp_path, "csv-kv")
        result = export_as_csv("csv-kv")
        csv_text = result["profile"]
        assert "key,value" in csv_text
        assert "session_id" in csv_text

    def test_csv_tasks_has_header_row(self, tmp_path):
        from brynq.data_export import export_as_csv
        _seed_task(tmp_path, "ct2", created_by="csv-task")
        result = export_as_csv("csv-task")
        csv_text = result["tasks"]
        assert "task_id" in csv_text
        assert "ct2" in csv_text

    def test_csv_empty_sections_excluded(self, tmp_path):
        from brynq.data_export import export_as_csv
        result = export_as_csv("no-data-user")
        # Empty sections should not appear
        assert "tasks" not in result
        assert "messages" not in result


# ── 3. Data Inventory ───────────────────────────────────────────────────


class TestDataInventory:
    def test_inventory_empty_user(self, tmp_path):
        from brynq.data_export import get_data_inventory
        inv = get_data_inventory("nobody")
        assert inv["profile"] == 0
        assert inv["tasks"] == 0

    def test_inventory_counts_profile(self, tmp_path):
        from brynq.data_export import get_data_inventory
        _seed_profile(tmp_path, "inv-user")
        inv = get_data_inventory("inv-user")
        assert inv["profile"] > 0

    def test_inventory_counts_tasks(self, tmp_path):
        from brynq.data_export import get_data_inventory
        _seed_task(tmp_path, "it1", created_by="inv-t")
        _seed_task(tmp_path, "it2", created_by="inv-t")
        inv = get_data_inventory("inv-t")
        assert inv["tasks"] == 2

    def test_inventory_requires_user_id(self):
        from brynq.data_export import get_data_inventory
        with pytest.raises(ValueError, match="user_id is required"):
            get_data_inventory("")


# ── 4. Delete User Data ─────────────────────────────────────────────────


class TestDeleteUserData:
    def test_delete_requires_confirm(self, tmp_path):
        from brynq.data_export import delete_user_data
        with pytest.raises(ValueError, match="confirm=True"):
            delete_user_data("del-user")

    def test_delete_requires_user_id(self):
        from brynq.data_export import delete_user_data
        with pytest.raises(ValueError, match="user_id is required"):
            delete_user_data("", confirm=True)

    def test_delete_removes_profile(self, tmp_path):
        from brynq.data_export import delete_user_data
        _seed_profile(tmp_path, "del-prof")
        result = delete_user_data("del-prof", confirm=True)
        assert "profile" in result
        assert not (tmp_path / "profiles" / "del-prof.json").exists()

    def test_delete_removes_tasks(self, tmp_path):
        from brynq.data_export import delete_user_data
        _seed_task(tmp_path, "dt1", created_by="del-tasks")
        result = delete_user_data("del-tasks", confirm=True)
        assert any("task:" in item for item in result)
        assert not (tmp_path / "tasks" / "dt1.json").exists()

    def test_delete_removes_channel_messages(self, tmp_path):
        from brynq.data_export import delete_user_data
        _seed_channel_msg(tmp_path, "general", "del-msg", "remove me")
        _seed_channel_msg(tmp_path, "general", "other", "keep me")
        result = delete_user_data("del-msg", confirm=True)
        assert any("messages:" in item for item in result)
        # The channel file should still exist but without the user's messages
        remaining = (tmp_path / "channels" / "general.jsonl").read_text("utf-8")
        assert "remove me" not in remaining
        assert "keep me" in remaining

    def test_delete_removes_dms(self, tmp_path):
        from brynq.data_export import delete_user_data
        _seed_dm(tmp_path, "conv-del", "del-dm", "secret")
        result = delete_user_data("del-dm", confirm=True)
        assert any("dms:" in item for item in result)

    def test_delete_removes_memory(self, tmp_path):
        from brynq.data_export import delete_user_data
        _seed_memory(tmp_path, "del-mem", "key1", "val1")
        result = delete_user_data("del-mem", confirm=True)
        assert "memory" in result
        assert not (tmp_path / "memory" / "del-mem").exists()

    def test_delete_removes_preferences(self, tmp_path):
        from brynq.data_export import delete_user_data
        _seed_prefs(tmp_path, "del-prefs")
        result = delete_user_data("del-prefs", confirm=True)
        assert "preferences" in result
        assert not (tmp_path / "notification_prefs" / "del-prefs.json").exists()

    def test_delete_removes_sandbox(self, tmp_path):
        from brynq.data_export import delete_user_data
        _seed_sandbox(tmp_path, "del-sb")
        result = delete_user_data("del-sb", confirm=True)
        assert "sandbox" in result
        assert not (tmp_path / "sandboxes" / "del-sb.json").exists()

    def test_delete_logs_to_audit(self, tmp_path):
        from brynq.data_export import delete_user_data
        _seed_profile(tmp_path, "del-audit")
        delete_user_data("del-audit", confirm=True)
        audit_path = tmp_path / "exports" / "audit.jsonl"
        entries = [json.loads(line) for line in audit_path.read_text("utf-8").strip().splitlines()]
        delete_entries = [e for e in entries if e["action"] == "delete"]
        assert len(delete_entries) == 1
        assert delete_entries[0]["user_id"] == "del-audit"

    def test_delete_empty_user_returns_empty(self, tmp_path):
        from brynq.data_export import delete_user_data
        result = delete_user_data("no-such-user", confirm=True)
        # No data exists, so nothing deleted (aside from audit log)
        assert isinstance(result, list)


# ── 5. Anonymize ────────────────────────────────────────────────────────


class TestAnonymize:
    def test_anonymize_profile(self, tmp_path):
        from brynq.data_export import anonymize_user_data
        _seed_profile(tmp_path, "anon-user")
        result = anonymize_user_data("anon-user")
        assert "profile" in result["sections_anonymized"]

        profile = json.loads((tmp_path / "profiles" / "anon-user.json").read_text("utf-8"))
        assert profile["bio"] == "Anonymized user"
        assert profile["anonymized"] is True
        assert profile["session_name"].startswith("anon-")

    def test_anonymize_keeps_stats(self, tmp_path):
        from brynq.data_export import anonymize_user_data
        _seed_profile(tmp_path, "anon-stats", extra={"tasks_completed": 42, "reputation": 4.8})
        anonymize_user_data("anon-stats")
        profile = json.loads((tmp_path / "profiles" / "anon-stats.json").read_text("utf-8"))
        assert profile["tasks_completed"] == 42
        assert profile["reputation"] == 4.8

    def test_anonymize_channel_messages(self, tmp_path):
        from brynq.data_export import anonymize_user_data
        _seed_channel_msg(tmp_path, "general", "anon-ch", "hello")
        result = anonymize_user_data("anon-ch")
        assert any("messages:" in s for s in result["sections_anonymized"])

        lines = (tmp_path / "channels" / "general.jsonl").read_text("utf-8").strip().splitlines()
        msg = json.loads(lines[0])
        assert msg["session_name"].startswith("anon-")

    def test_anonymize_requires_user_id(self):
        from brynq.data_export import anonymize_user_data
        with pytest.raises(ValueError, match="user_id is required"):
            anonymize_user_data("")

    def test_anonymize_returns_anon_id(self, tmp_path):
        from brynq.data_export import anonymize_user_data
        _seed_profile(tmp_path, "anon-ret")
        result = anonymize_user_data("anon-ret")
        assert result["anon_id"].startswith("anon-")
        assert result["user_id"] == "anon-ret"


# ── 6. Backup & Restore ─────────────────────────────────────────────────


class TestBackupRestore:
    def test_create_backup_all_sections(self, tmp_path):
        from brynq.data_export import create_backup
        _seed_profile(tmp_path, "bk-user")
        result = create_backup()
        assert "backup_id" in result
        assert result["file_count"] >= 1
        assert "profiles" in result["sections"]

    def test_create_backup_specific_sections(self, tmp_path):
        from brynq.data_export import create_backup
        _seed_profile(tmp_path, "bk-user2")
        _seed_task(tmp_path, "bkt1", created_by="bk-user2")
        result = create_backup(sections=["profiles", "tasks"])
        assert set(result["sections"]) == {"profiles", "tasks"}

    def test_list_backups(self, tmp_path):
        from brynq.data_export import create_backup, list_backups
        create_backup()
        create_backup()
        backups = list_backups()
        assert len(backups) >= 2

    def test_list_backups_empty(self, tmp_path):
        from brynq.data_export import list_backups
        backups = list_backups()
        assert backups == []

    def test_restore_backup(self, tmp_path):
        from brynq.data_export import create_backup, restore_backup
        _seed_profile(tmp_path, "restore-user")
        backup = create_backup(sections=["profiles"])

        # Delete the profile
        (tmp_path / "profiles" / "restore-user.json").unlink()
        assert not (tmp_path / "profiles" / "restore-user.json").exists()

        # Restore from backup
        result = restore_backup(backup["backup_id"])
        assert "profiles" in result["sections_restored"]
        assert (tmp_path / "profiles" / "restore-user.json").exists()

    def test_restore_nonexistent_backup(self, tmp_path):
        from brynq.data_export import restore_backup
        with pytest.raises(FileNotFoundError, match="Backup not found"):
            restore_backup("nonexistent-backup-id")

    def test_restore_specific_sections(self, tmp_path):
        from brynq.data_export import create_backup, restore_backup
        _seed_profile(tmp_path, "rs-user")
        _seed_task(tmp_path, "rst1", created_by="rs-user")
        backup = create_backup(sections=["profiles", "tasks"])

        # Delete both
        (tmp_path / "profiles" / "rs-user.json").unlink()
        (tmp_path / "tasks" / "rst1.json").unlink()

        # Restore only profiles
        result = restore_backup(backup["backup_id"], sections=["profiles"])
        assert "profiles" in result["sections_restored"]
        assert (tmp_path / "profiles" / "rs-user.json").exists()


# ── 7. Export Request Tracking ───────────────────────────────────────────


class TestExportRequests:
    def test_request_export(self, tmp_path):
        from brynq.data_export import request_export
        result = request_export("req-user")
        assert result["user_id"] == "req-user"
        assert result["status"] == "pending"
        assert result["request_id"]

    def test_request_export_requires_user_id(self):
        from brynq.data_export import request_export
        with pytest.raises(ValueError, match="user_id is required"):
            request_export("")

    def test_get_export_requests_all(self, tmp_path):
        from brynq.data_export import request_export, get_export_requests
        request_export("user1")
        request_export("user2")
        results = get_export_requests()
        assert len(results) == 2

    def test_get_export_requests_by_status(self, tmp_path):
        from brynq.data_export import request_export, get_export_requests, complete_export
        r1 = request_export("user-filter1")
        request_export("user-filter2")
        complete_export(r1["request_id"])
        pending = get_export_requests(status="pending")
        completed = get_export_requests(status="completed")
        assert len(pending) == 1
        assert len(completed) == 1

    def test_complete_export(self, tmp_path):
        from brynq.data_export import request_export, complete_export
        req = request_export("comp-user")
        result = complete_export(req["request_id"], notes="All done")
        assert result["status"] == "completed"
        assert result["completed_at"] is not None
        assert result["notes"] == "All done"

    def test_complete_nonexistent_request(self, tmp_path):
        from brynq.data_export import complete_export
        with pytest.raises(ValueError, match="Export request not found"):
            complete_export("no-such-id")
