"""Tests for rbsp.indexer.incremental -- _FileState, IncrementalEngine, _ScanStats."""

import json
import os
import sys
import unittest

sys.path.insert(0, os.path.dirname(__file__))
from conftest import RBSPTestCase

from rbsp.indexer.incremental import _FileState, IncrementalEngine, _ScanStats


# ---------------------------------------------------------------------------
# _FileState
# ---------------------------------------------------------------------------

class TestFileStateToDict(unittest.TestCase):
    """_FileState.to_dict round-trip tests."""

    def test_to_dict_all_fields(self):
        fs = _FileState(mtime=1000.5, size=42, content_hash="abc123")
        d = fs.to_dict()
        self.assertEqual(d["mtime"], 1000.5)
        self.assertEqual(d["size"], 42)
        self.assertEqual(d["hash"], "abc123")

    def test_to_dict_empty_hash(self):
        fs = _FileState(mtime=0.0, size=0)
        d = fs.to_dict()
        self.assertEqual(d["hash"], "")

    def test_from_dict_all_fields(self):
        d = {"mtime": 2000.0, "size": 100, "hash": "deadbeef"}
        fs = _FileState.from_dict(d)
        self.assertEqual(fs.mtime, 2000.0)
        self.assertEqual(fs.size, 100)
        self.assertEqual(fs.content_hash, "deadbeef")

    def test_from_dict_missing_hash(self):
        d = {"mtime": 1.0, "size": 1}
        fs = _FileState.from_dict(d)
        self.assertEqual(fs.content_hash, "")

    def test_round_trip(self):
        original = _FileState(mtime=12345.678, size=9999, content_hash="ff00ff")
        restored = _FileState.from_dict(original.to_dict())
        self.assertEqual(restored.mtime, original.mtime)
        self.assertEqual(restored.size, original.size)
        self.assertEqual(restored.content_hash, original.content_hash)


# ---------------------------------------------------------------------------
# _ScanStats
# ---------------------------------------------------------------------------

class TestScanStats(unittest.TestCase):
    """_ScanStats defaults and repr."""

    def test_defaults_all_zero(self):
        ss = _ScanStats()
        self.assertEqual(ss.total, 0)
        self.assertEqual(ss.indexed, 0)
        self.assertEqual(ss.skipped, 0)
        self.assertEqual(ss.deleted, 0)
        self.assertEqual(ss.errors, 0)

    def test_repr(self):
        ss = _ScanStats()
        ss.total = 10
        ss.indexed = 5
        ss.skipped = 3
        ss.deleted = 1
        ss.errors = 1
        r = repr(ss)
        self.assertIn("total=10", r)
        self.assertIn("indexed=5", r)
        self.assertIn("skipped=3", r)
        self.assertIn("deleted=1", r)
        self.assertIn("errors=1", r)


# ---------------------------------------------------------------------------
# IncrementalEngine -- process_file with real temp files
# ---------------------------------------------------------------------------

class TestIncrementalEngineProcessFile(RBSPTestCase):
    """process_file with real temporary files."""

    def test_new_file_is_indexed(self):
        path = self.make_sample_file("hello.txt", "hello world")
        indexed_paths = []
        engine = IncrementalEngine(on_index=lambda p, c: indexed_paths.append(p))
        result = engine.process_file(path)
        self.assertTrue(result)
        self.assertEqual(len(indexed_paths), 1)
        self.assertEqual(indexed_paths[0], os.path.abspath(path))

    def test_unchanged_file_is_skipped(self):
        path = self.make_sample_file("stable.txt", "no change")
        calls = []
        engine = IncrementalEngine(on_index=lambda p, c: calls.append(p))
        # First call indexes
        self.assertTrue(engine.process_file(path))
        self.assertEqual(len(calls), 1)
        # Second call (no mtime/size change) should skip
        self.assertFalse(engine.process_file(path))
        self.assertEqual(len(calls), 1)

    def test_modified_file_is_reindexed(self):
        path = self.make_sample_file("change.txt", "version 1")
        calls = []
        engine = IncrementalEngine(on_index=lambda p, c: calls.append(c))
        engine.process_file(path)
        # Modify the file -- change size so mtime/size differ
        with open(path, "w", encoding="utf-8") as f:
            f.write("version 2 -- longer content to ensure size differs")
        result = engine.process_file(path)
        self.assertTrue(result)
        self.assertEqual(len(calls), 2)

    def test_callback_receives_content(self):
        content_text = "sample data for hashing"
        path = self.make_sample_file("data.txt", content_text)
        received = []
        engine = IncrementalEngine(on_index=lambda p, c: received.append(c))
        engine.process_file(path)
        self.assertEqual(received[0], content_text.encode("utf-8"))

    def test_no_callback_still_works(self):
        path = self.make_sample_file("nocb.txt", "test")
        engine = IncrementalEngine()
        result = engine.process_file(path)
        self.assertTrue(result)

    def test_nonexistent_file_returns_false(self):
        engine = IncrementalEngine()
        result = engine.process_file(os.path.join(self.tmp_dir, "nope.txt"))
        self.assertFalse(result)


# ---------------------------------------------------------------------------
# IncrementalEngine -- tracked_count and clear_state
# ---------------------------------------------------------------------------

class TestIncrementalEngineTrackedCount(RBSPTestCase):
    """tracked_count property."""

    def test_initially_zero(self):
        engine = IncrementalEngine()
        self.assertEqual(engine.tracked_count, 0)

    def test_increments_after_indexing(self):
        engine = IncrementalEngine()
        p1 = self.make_sample_file("a.txt", "aaa")
        p2 = self.make_sample_file("b.txt", "bbb")
        engine.process_file(p1)
        engine.process_file(p2)
        self.assertEqual(engine.tracked_count, 2)

    def test_clear_state_resets_count(self):
        engine = IncrementalEngine()
        engine.process_file(self.make_sample_file("x.txt", "data"))
        self.assertEqual(engine.tracked_count, 1)
        engine.clear_state()
        self.assertEqual(engine.tracked_count, 0)

    def test_clear_state_forces_reindex(self):
        path = self.make_sample_file("reindex.txt", "original")
        calls = []
        engine = IncrementalEngine(on_index=lambda p, c: calls.append(1))
        engine.process_file(path)
        self.assertEqual(len(calls), 1)
        engine.clear_state()
        engine.process_file(path)
        self.assertEqual(len(calls), 2)


# ---------------------------------------------------------------------------
# IncrementalEngine -- save_state / load_state persistence
# ---------------------------------------------------------------------------

class TestIncrementalEnginePersistence(RBSPTestCase):
    """save_state / load_state via state_path."""

    def test_save_and_load_state(self):
        state_file = os.path.join(self.tmp_dir, "state.json")
        path = self.make_sample_file("persist.txt", "hello")

        engine1 = IncrementalEngine(state_path=state_file)
        engine1.process_file(path)
        engine1.save_state()

        # State file should exist
        self.assertTrue(os.path.exists(state_file))

        # New engine loading the state should skip the unchanged file
        calls = []
        engine2 = IncrementalEngine(
            state_path=state_file,
            on_index=lambda p, c: calls.append(1),
        )
        self.assertEqual(engine2.tracked_count, 1)
        result = engine2.process_file(path)
        self.assertFalse(result)
        self.assertEqual(len(calls), 0)

    def test_save_state_creates_parent_dirs(self):
        state_file = os.path.join(self.tmp_dir, "sub", "dir", "state.json")
        engine = IncrementalEngine(state_path=state_file)
        engine.process_file(self.make_sample_file("f.txt", "content"))
        engine.save_state()
        self.assertTrue(os.path.exists(state_file))

    def test_save_state_no_path_is_noop(self):
        engine = IncrementalEngine(state_path=None)
        engine.process_file(self.make_sample_file("f.txt", "content"))
        # Should not raise
        engine.save_state()

    def test_clear_state_removes_file(self):
        state_file = os.path.join(self.tmp_dir, "to_remove.json")
        engine = IncrementalEngine(state_path=state_file)
        engine.process_file(self.make_sample_file("f.txt", "content"))
        engine.save_state()
        self.assertTrue(os.path.exists(state_file))
        engine.clear_state()
        self.assertFalse(os.path.exists(state_file))

    def test_load_corrupt_state_starts_fresh(self):
        state_file = os.path.join(self.tmp_dir, "bad.json")
        with open(state_file, "w") as f:
            f.write("NOT VALID JSON{{{")
        engine = IncrementalEngine(state_path=state_file)
        self.assertEqual(engine.tracked_count, 0)

    def test_state_file_contents_are_valid_json(self):
        state_file = os.path.join(self.tmp_dir, "check.json")
        path = self.make_sample_file("j.txt", "json check")
        engine = IncrementalEngine(state_path=state_file)
        engine.process_file(path)
        engine.save_state()

        with open(state_file, "r", encoding="utf-8") as f:
            data = json.load(f)
        abs_path = os.path.abspath(path)
        self.assertIn(abs_path, data)
        self.assertIn("mtime", data[abs_path])
        self.assertIn("size", data[abs_path])
        self.assertIn("hash", data[abs_path])


# ---------------------------------------------------------------------------
# IncrementalEngine -- process_delete
# ---------------------------------------------------------------------------

class TestIncrementalEngineProcessDelete(RBSPTestCase):
    """process_delete behaviour."""

    def test_delete_removes_tracked_entry(self):
        path = self.make_sample_file("del.txt", "to delete")
        engine = IncrementalEngine()
        engine.process_file(path)
        self.assertEqual(engine.tracked_count, 1)
        engine.process_delete(path)
        self.assertEqual(engine.tracked_count, 0)

    def test_delete_calls_on_remove(self):
        path = self.make_sample_file("rm.txt", "data")
        removed = []
        engine = IncrementalEngine(on_remove=lambda p: removed.append(p))
        engine.process_file(path)
        engine.process_delete(path)
        self.assertEqual(len(removed), 1)

    def test_delete_untracked_is_harmless(self):
        engine = IncrementalEngine()
        engine.process_delete("/nonexistent/file.txt")
        # Should not raise; tracked_count stays zero
        self.assertEqual(engine.tracked_count, 0)


# ---------------------------------------------------------------------------
# IncrementalEngine -- stats property
# ---------------------------------------------------------------------------

class TestIncrementalEngineStats(RBSPTestCase):
    """stats property tracks indexed/skipped counts."""

    def test_stats_after_process_file(self):
        path = self.make_sample_file("s.txt", "stats")
        engine = IncrementalEngine()
        engine.process_file(path)
        self.assertEqual(engine.stats.indexed, 1)
        # Process again -- should skip
        engine.process_file(path)
        self.assertEqual(engine.stats.skipped, 1)


if __name__ == "__main__":
    unittest.main()
