"""Tests for intelligence.learning.storage -- SQLite persistence layer."""

from __future__ import annotations

from orxaq.intelligence.learning.experiment import Experiment, ExperimentTracker
from orxaq.intelligence.learning.metrics import DiscoveryMetrics, MetricCollector
from orxaq.intelligence.learning.storage import LearningDB


class TestLearningDBCreation:
    def test_creates_db_file(self, tmp_path):
        db_path = tmp_path / "test.db"
        db = LearningDB(db_path=db_path)
        assert db_path.exists()
        db.close()

    def test_creates_tables(self, tmp_path):
        db_path = tmp_path / "test.db"
        db = LearningDB(db_path=db_path)
        # Verify tables exist by querying them
        rows = db.load_metrics()
        assert rows == []
        exps = db.load_experiments()
        assert exps == []
        db.close()

    def test_default_db_path(self, tmp_path, monkeypatch):
        # Monkeypatch home to avoid touching real ~/.orxaq
        import orxaq.intelligence.learning.storage as storage_mod

        fake_dir = tmp_path / ".orxaq"
        monkeypatch.setattr(storage_mod, "_DEFAULT_DB_DIR", fake_dir)
        monkeypatch.setattr(storage_mod, "_DEFAULT_DB_PATH", fake_dir / "learning.db")
        db = LearningDB(db_path=None)
        assert (fake_dir / "learning.db").exists()
        db.close()

    def test_creates_parent_dirs(self, tmp_path):
        db_path = tmp_path / "deep" / "nested" / "dir" / "test.db"
        db = LearningDB(db_path=db_path)
        assert db_path.exists()
        db.close()


class TestLearningDBMetrics:
    def _make_metrics(self, run_id: str = "r1", **kwargs) -> DiscoveryMetrics:
        defaults = {
            "run_id": run_id,
            "timestamp": 1000.0,
            "algorithm": "pc",
            "dataset_size": 500,
            "num_variables": 10,
            "shd": 3,
            "edge_precision": 0.85,
            "edge_recall": 0.90,
            "edge_f1": 0.8743,
            "orientation_accuracy": 0.75,
            "execution_time_ms": 123.45,
            "config": {"alpha": 0.05},
        }
        defaults.update(kwargs)
        return DiscoveryMetrics(**defaults)

    def test_save_and_load_roundtrip(self, tmp_path):
        db = LearningDB(db_path=tmp_path / "test.db")
        m = self._make_metrics()
        db.save_metric(m)
        rows = db.load_metrics()
        assert len(rows) == 1
        r = rows[0]
        assert r["run_id"] == "r1"
        assert r["algorithm"] == "pc"
        assert r["shd"] == 3
        assert abs(r["edge_precision"] - 0.85) < 1e-6
        assert abs(r["edge_recall"] - 0.90) < 1e-6
        assert r["config"] == {"alpha": 0.05}
        db.close()

    def test_load_metric_by_id(self, tmp_path):
        db = LearningDB(db_path=tmp_path / "test.db")
        db.save_metric(self._make_metrics("r1"))
        db.save_metric(self._make_metrics("r2", algorithm="ges"))
        r = db.load_metric_by_id("r2")
        assert r is not None
        assert r["run_id"] == "r2"
        assert r["algorithm"] == "ges"
        assert db.load_metric_by_id("nonexistent") is None
        db.close()

    def test_algorithm_filter(self, tmp_path):
        db = LearningDB(db_path=tmp_path / "test.db")
        db.save_metric(self._make_metrics("r1", algorithm="pc"))
        db.save_metric(self._make_metrics("r2", algorithm="ges"))
        db.save_metric(self._make_metrics("r3", algorithm="pc"))
        pc_rows = db.load_metrics(algorithm="pc")
        assert len(pc_rows) == 2
        assert all(r["algorithm"] == "pc" for r in pc_rows)
        db.close()

    def test_limit(self, tmp_path):
        db = LearningDB(db_path=tmp_path / "test.db")
        for i in range(10):
            db.save_metric(self._make_metrics(f"r{i}", timestamp=float(i)))
        rows = db.load_metrics(limit=3)
        assert len(rows) == 3
        db.close()

    def test_upsert_replaces(self, tmp_path):
        db = LearningDB(db_path=tmp_path / "test.db")
        db.save_metric(self._make_metrics("r1", shd=5))
        db.save_metric(self._make_metrics("r1", shd=2))
        rows = db.load_metrics()
        assert len(rows) == 1
        assert rows[0]["shd"] == 2
        db.close()


class TestLearningDBExperiments:
    def _make_experiment(self, eid: str = "e1", **kwargs) -> Experiment:
        defaults = {
            "experiment_id": eid,
            "name": "test run",
            "algorithm": "pc",
            "algorithm_config": {"alpha": 0.05},
            "data_config": {"n": 1000},
            "results": {"shd": 3},
            "start_time": 1000.0,
            "end_time": 1005.0,
            "status": "completed",
            "tags": ["baseline"],
        }
        defaults.update(kwargs)
        return Experiment(**defaults)

    def test_save_and_load_roundtrip(self, tmp_path):
        db = LearningDB(db_path=tmp_path / "test.db")
        exp = self._make_experiment()
        db.save_experiment(exp)
        rows = db.load_experiments()
        assert len(rows) == 1
        r = rows[0]
        assert r["experiment_id"] == "e1"
        assert r["name"] == "test run"
        assert r["algorithm"] == "pc"
        assert r["algorithm_config"] == {"alpha": 0.05}
        assert r["data_config"] == {"n": 1000}
        assert r["results"] == {"shd": 3}
        assert r["status"] == "completed"
        assert r["tags"] == ["baseline"]
        db.close()

    def test_status_filter(self, tmp_path):
        db = LearningDB(db_path=tmp_path / "test.db")
        db.save_experiment(self._make_experiment("e1", status="completed"))
        db.save_experiment(self._make_experiment("e2", status="pending"))
        db.save_experiment(self._make_experiment("e3", status="completed"))
        completed = db.load_experiments(status="completed")
        assert len(completed) == 2
        assert all(r["status"] == "completed" for r in completed)
        db.close()

    def test_limit(self, tmp_path):
        db = LearningDB(db_path=tmp_path / "test.db")
        for i in range(10):
            db.save_experiment(self._make_experiment(f"e{i}", start_time=float(i)))
        rows = db.load_experiments(limit=3)
        assert len(rows) == 3
        db.close()

    def test_upsert_replaces(self, tmp_path):
        db = LearningDB(db_path=tmp_path / "test.db")
        db.save_experiment(self._make_experiment("e1", status="pending"))
        db.save_experiment(self._make_experiment("e1", status="completed"))
        rows = db.load_experiments()
        assert len(rows) == 1
        assert rows[0]["status"] == "completed"
        db.close()


class TestLearningDBSummary:
    def test_summary_empty(self, tmp_path):
        db = LearningDB(db_path=tmp_path / "test.db")
        s = db.summary()
        assert s["metric_runs"] == 0
        assert s["algorithms"] == []
        assert s["best_shd"] is None
        assert s["best_f1"] is None
        assert s["experiments"] == 0
        assert s["experiments_by_status"] == {}
        db.close()

    def test_summary_with_data(self, tmp_path):
        db = LearningDB(db_path=tmp_path / "test.db")
        db.save_metric(
            DiscoveryMetrics(
                run_id="r1",
                algorithm="pc",
                shd=5,
                edge_f1=0.7,
                execution_time_ms=100,
                timestamp=1000.0,
            )
        )
        db.save_metric(
            DiscoveryMetrics(
                run_id="r2",
                algorithm="ges",
                shd=2,
                edge_f1=0.9,
                execution_time_ms=200,
                timestamp=1001.0,
            )
        )
        db.save_experiment(
            Experiment(
                experiment_id="e1",
                name="test",
                algorithm="pc",
                status="completed",
                start_time=1000.0,
                end_time=1005.0,
            )
        )
        db.save_experiment(
            Experiment(
                experiment_id="e2",
                name="test2",
                algorithm="ges",
                status="pending",
                start_time=1001.0,
            )
        )

        s = db.summary()
        assert s["metric_runs"] == 2
        assert sorted(s["algorithms"]) == ["ges", "pc"]
        assert s["best_shd"] == 2
        assert s["best_f1"] == 0.9
        assert s["avg_execution_ms"] == 150.0
        assert s["experiments"] == 2
        assert s["experiments_by_status"]["completed"] == 1
        assert s["experiments_by_status"]["pending"] == 1
        db.close()


class TestMetricCollectorWithDB:
    def test_record_persists_to_db(self, tmp_path):
        db = LearningDB(db_path=tmp_path / "test.db")
        mc = MetricCollector(db=db)
        m = DiscoveryMetrics(run_id="r1", algorithm="pc", shd=3, timestamp=1000.0)
        mc.record(m)
        rows = db.load_metrics()
        assert len(rows) == 1
        assert rows[0]["run_id"] == "r1"
        db.close()

    def test_record_without_db(self):
        mc = MetricCollector()
        m = DiscoveryMetrics(run_id="r1", algorithm="pc", shd=3)
        mc.record(m)
        assert len(mc.runs) == 1


class TestExperimentTrackerWithDB:
    def test_complete_persists_to_db(self, tmp_path):
        db = LearningDB(db_path=tmp_path / "test.db")
        tracker = ExperimentTracker(db=db)
        tracker.create("e1", "test", "pc")
        tracker.start("e1")
        tracker.complete("e1", {"shd": 2})
        rows = db.load_experiments()
        assert len(rows) == 1
        assert rows[0]["experiment_id"] == "e1"
        assert rows[0]["status"] == "completed"
        db.close()

    def test_complete_without_db(self):
        tracker = ExperimentTracker()
        tracker.create("e1", "test", "pc")
        tracker.start("e1")
        exp = tracker.complete("e1", {"shd": 2})
        assert exp.status == "completed"
