# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# Licensed under the MIT License

"""Tests for the universal FileStore."""

import hashlib
import threading

import pytest

from familiar.core.files import (
    MAX_FILE_SIZE,
    FileRecord,
    FileStore,
    _sanitize_filename,
    _sanitize_path_component,
)


@pytest.fixture
def store(tmp_path):
    """Create a FileStore with a temporary directory."""
    return FileStore(files_dir=tmp_path / "files")


@pytest.fixture
def sample_pdf():
    """Minimal PDF-like bytes."""
    return b"%PDF-1.4 sample content for testing"


# ── Sanitization ──


class TestSanitization:
    def test_sanitize_filename_basic(self):
        assert _sanitize_filename("report.pdf") == "report.pdf"

    def test_sanitize_filename_strips_path(self):
        assert _sanitize_filename("../../etc/passwd") == ".._.._etc_passwd"

    def test_sanitize_filename_strips_special(self):
        result = _sanitize_filename("file<>name|.pdf")
        assert "<" not in result
        assert ">" not in result
        assert "|" not in result

    def test_sanitize_filename_collapses_whitespace(self):
        assert _sanitize_filename("my  file  name.pdf") == "my_file_name.pdf"

    def test_sanitize_filename_empty(self):
        assert _sanitize_filename("") == "unnamed"

    def test_sanitize_filename_long(self):
        name = "a" * 300 + ".pdf"
        result = _sanitize_filename(name)
        assert len(result) <= 200

    def test_sanitize_path_component(self):
        assert _sanitize_path_component("social_worker") == "social_worker"
        assert "/" not in _sanitize_path_component("../../bad")
        assert _sanitize_path_component("") == "unknown"


# ── FileRecord ──


class TestFileRecord:
    def test_to_dict(self):
        r = FileRecord(
            file_id="abc",
            job_class="nonprofit",
            record_type="grants",
            record_id="g1",
            filename="app.pdf",
            mime_type="application/pdf",
            size_bytes=100,
            sha256="deadbeef",
            stored_at="2026-01-01T00:00:00Z",
        )
        d = r.to_dict()
        assert d["file_id"] == "abc"
        assert d["job_class"] == "nonprofit"
        assert d["description"] == ""

    def test_default_description(self):
        r = FileRecord(
            file_id="x",
            job_class="j",
            record_type="r",
            record_id="i",
            filename="f",
            mime_type="m",
            size_bytes=0,
            sha256="s",
            stored_at="t",
        )
        assert r.description == ""


# ── Store / Get / Delete ──


class TestStoreOperations:
    def test_store_and_get(self, store, sample_pdf):
        rec = store.store(
            data=sample_pdf,
            filename="application.pdf",
            job_class="nonprofit",
            record_type="grants",
            record_id="grant-001",
        )
        assert rec.file_id
        assert rec.filename == "application.pdf"
        assert rec.mime_type == "application/pdf"
        assert rec.size_bytes == len(sample_pdf)
        assert rec.sha256 == hashlib.sha256(sample_pdf).hexdigest()

        # Retrieve by ID
        got = store.get(rec.file_id)
        assert got is not None
        assert got.file_id == rec.file_id

    def test_get_nonexistent(self, store):
        assert store.get("no-such-id") is None

    def test_get_path(self, store, sample_pdf):
        rec = store.store(
            data=sample_pdf,
            filename="test.pdf",
            job_class="nonprofit",
            record_type="grants",
            record_id="g1",
        )
        path = store.get_path(rec.file_id)
        assert path is not None
        assert path.exists()
        assert path.read_bytes() == sample_pdf

    def test_get_path_nonexistent(self, store):
        assert store.get_path("no-such-id") is None

    def test_delete(self, store, sample_pdf):
        rec = store.store(
            data=sample_pdf,
            filename="test.pdf",
            job_class="nonprofit",
            record_type="grants",
            record_id="g1",
        )
        path = store.get_path(rec.file_id)
        assert path.exists()

        assert store.delete(rec.file_id) is True
        assert store.get(rec.file_id) is None
        assert not path.exists()

    def test_delete_nonexistent(self, store):
        assert store.delete("no-such-id") is False

    def test_store_with_description(self, store, sample_pdf):
        rec = store.store(
            data=sample_pdf,
            filename="budget.xlsx",
            job_class="nonprofit",
            record_type="grants",
            record_id="g1",
            description="FY2026 operating budget",
        )
        got = store.get(rec.file_id)
        assert got.description == "FY2026 operating budget"

    def test_directory_structure(self, store, sample_pdf):
        rec = store.store(
            data=sample_pdf,
            filename="test.pdf",
            job_class="nonprofit",
            record_type="grants",
            record_id="g1",
        )
        path = store.get_path(rec.file_id)
        # Should be files/nonprofit/grants/g1/{id}_test.pdf
        parts = path.parts
        assert "nonprofit" in parts
        assert "grants" in parts
        assert "g1" in parts


# ── Validation ──


class TestValidation:
    def test_reject_oversized_file(self, store):
        big = b"x" * (MAX_FILE_SIZE + 1)
        with pytest.raises(ValueError, match="too large"):
            store.store(
                data=big,
                filename="huge.pdf",
                job_class="test",
                record_type="test",
                record_id="t1",
            )

    def test_reject_disallowed_mime(self, store):
        with pytest.raises(ValueError, match="not allowed"):
            store.store(
                data=b"\x00\x01\x02\x03",
                filename="malware.exe",
                job_class="test",
                record_type="test",
                record_id="t1",
            )

    def test_allow_pdf(self, store, sample_pdf):
        rec = store.store(
            data=sample_pdf,
            filename="doc.pdf",
            job_class="test",
            record_type="test",
            record_id="t1",
        )
        assert rec.mime_type == "application/pdf"

    def test_allow_text(self, store):
        rec = store.store(
            data=b"hello world",
            filename="notes.txt",
            job_class="test",
            record_type="test",
            record_id="t1",
        )
        assert rec.mime_type.startswith("text/")

    def test_allow_image(self, store):
        # PNG magic bytes
        png = b"\x89PNG\r\n\x1a\n" + b"\x00" * 100
        rec = store.store(
            data=png,
            filename="photo.png",
            job_class="test",
            record_type="test",
            record_id="t1",
        )
        assert rec.mime_type == "image/png"


# ── Listing / Search ──


class TestListing:
    def test_list_for_record(self, store, sample_pdf):
        store.store(sample_pdf, "a.pdf", "np", "grants", "g1")
        store.store(sample_pdf, "b.pdf", "np", "grants", "g1")
        store.store(sample_pdf, "c.pdf", "np", "grants", "g2")

        files = store.list_for_record("grants", "g1")
        assert len(files) == 2

        files = store.list_for_record("grants", "g2")
        assert len(files) == 1

    def test_list_for_record_empty(self, store):
        assert store.list_for_record("grants", "nonexistent") == []

    def test_list_for_job_class(self, store, sample_pdf):
        store.store(sample_pdf, "a.pdf", "nonprofit", "grants", "g1")
        store.store(sample_pdf, "b.pdf", "nonprofit", "donors", "d1")
        store.store(sample_pdf, "c.pdf", "social_worker", "cases", "c1")

        np_files = store.list_for_job_class("nonprofit")
        assert len(np_files) == 2

        sw_files = store.list_for_job_class("social_worker")
        assert len(sw_files) == 1

    def test_search_by_filename(self, store, sample_pdf):
        store.store(sample_pdf, "budget_2026.pdf", "np", "grants", "g1")
        store.store(sample_pdf, "application.pdf", "np", "grants", "g1")

        results = store.search(query="budget")
        assert len(results) == 1
        assert results[0].filename == "budget_2026.pdf"

    def test_search_by_description(self, store, sample_pdf):
        store.store(
            sample_pdf,
            "doc.pdf",
            "np",
            "grants",
            "g1",
            description="annual report for board",
        )
        results = store.search(query="board")
        assert len(results) == 1


# ── Integrity ──


class TestIntegrity:
    def test_verify_integrity_pass(self, store, sample_pdf):
        rec = store.store(sample_pdf, "test.pdf", "np", "grants", "g1")
        assert store.verify_integrity(rec.file_id) is True

    def test_verify_integrity_fail(self, store, sample_pdf):
        rec = store.store(sample_pdf, "test.pdf", "np", "grants", "g1")
        # Corrupt the file
        path = store.get_path(rec.file_id)
        path.write_bytes(b"corrupted data")
        assert store.verify_integrity(rec.file_id) is False

    def test_verify_nonexistent(self, store):
        assert store.verify_integrity("no-such-id") is False


# ── Memory Mentions ──


class TestMentions:
    def test_add_and_get_mentions(self, store, sample_pdf):
        rec = store.store(sample_pdf, "test.pdf", "np", "grants", "g1")
        store.add_mention(rec.file_id, "mem-001", "submitted")
        store.add_mention(rec.file_id, "mem-002", "reviewed")

        mentions = store.get_mentions(rec.file_id)
        assert len(mentions) == 2
        assert mentions[0]["memory_id"] == "mem-001"
        assert mentions[0]["context"] == "submitted"

    def test_duplicate_mention_ignored(self, store, sample_pdf):
        rec = store.store(sample_pdf, "test.pdf", "np", "grants", "g1")
        store.add_mention(rec.file_id, "mem-001")
        store.add_mention(rec.file_id, "mem-001")  # duplicate
        assert len(store.get_mentions(rec.file_id)) == 1

    def test_mentions_deleted_with_file(self, store, sample_pdf):
        rec = store.store(sample_pdf, "test.pdf", "np", "grants", "g1")
        store.add_mention(rec.file_id, "mem-001")
        store.delete(rec.file_id)
        assert store.get_mentions(rec.file_id) == []


# ── Stats ──


class TestStats:
    def test_stats_empty(self, store):
        s = store.stats()
        assert s["total_files"] == 0
        assert s["total_bytes"] == 0
        assert s["by_job_class"] == {}

    def test_stats_with_files(self, store, sample_pdf):
        store.store(sample_pdf, "a.pdf", "nonprofit", "grants", "g1")
        store.store(sample_pdf, "b.pdf", "social_worker", "cases", "c1")

        s = store.stats()
        assert s["total_files"] == 2
        assert s["total_bytes"] == len(sample_pdf) * 2
        assert "nonprofit" in s["by_job_class"]
        assert "social_worker" in s["by_job_class"]


# ── Thread Safety ──


class TestConcurrency:
    def test_concurrent_stores(self, store):
        data = b"concurrent test data for pdf" * 10
        errors = []

        def do_store(i):
            try:
                store.store(
                    data=data,
                    filename=f"file_{i}.txt",
                    job_class="test",
                    record_type="concurrent",
                    record_id=f"r{i}",
                )
            except Exception as e:
                errors.append(e)

        threads = [threading.Thread(target=do_store, args=(i,)) for i in range(10)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert errors == []
        files = store.search(job_class="test")
        assert len(files) == 10
