"""Tests for cache formatting functions and staleness helpers."""

import json
from datetime import datetime, timezone, timedelta
from pathlib import Path

import yaml

from gjalla_precommit.commands._cache import (
    _format_rules_md,
    _format_architecture_txt,
    _is_cache_stale,
    write_cache_files,
)


class TestFormatRulesMd:
    """Tests for _format_rules_md."""

    def test_formats_rules_with_rationale(self):
        rules = [
            {"name": "no-source-code-in-db", "status": "active", "rationale": "Keep DB clean"},
            {"name": "separation-of-concerns", "status": "active", "rationale": "Modularity"},
        ]
        result = _format_rules_md(rules)
        assert "# Project Rules" in result
        assert "**no-source-code-in-db** (active) — Keep DB clean" in result
        assert "**separation-of-concerns** (active) — Modularity" in result

    def test_formats_rules_without_rationale(self):
        rules = [{"name": "simple-rule", "status": "draft"}]
        result = _format_rules_md(rules)
        assert "**simple-rule** (draft)" in result
        assert "—" not in result

    def test_uses_description_as_fallback(self):
        rules = [{"name": "rule-1", "status": "active", "description": "Fallback desc"}]
        result = _format_rules_md(rules)
        assert "Fallback desc" in result

    def test_empty_rules(self):
        result = _format_rules_md([])
        assert "No rules defined" in result

    def test_handles_missing_fields(self):
        rules = [{}]
        result = _format_rules_md(rules)
        assert "unnamed" in result
        assert "unknown" in result


class TestFormatArchitectureTxt:
    """Tests for _format_architecture_txt."""

    def test_formats_dict_with_elements_and_connections(self):
        arch = {
            "elements": [
                {"name": "web-api", "type": "service", "description": "Main API"},
                {"name": "database", "type": "store", "description": "PostgreSQL"},
            ],
            "connections": [
                {"source": "web-api", "target": "database", "label": "queries"},
            ],
            "decisions": [
                {"title": "Use PostgreSQL", "status": "accepted"},
            ],
        }
        result = _format_architecture_txt(arch)
        assert "Architecture Overview" in result
        assert "[dim]\\[service][/dim] web-api — Main API" in result
        assert "[dim]\\[store][/dim] database — PostgreSQL" in result
        assert "web-api -> database (queries)" in result
        assert "Use PostgreSQL [accepted]" in result

    def test_formats_list_of_elements(self):
        arch = [
            {"name": "frontend", "type": "app"},
            {"name": "backend", "type": "service"},
        ]
        result = _format_architecture_txt(arch)
        assert "frontend" in result
        assert "backend" in result

    def test_empty_architecture(self):
        result = _format_architecture_txt({})
        assert "No architecture data available" in result

    def test_empty_list(self):
        result = _format_architecture_txt([])
        assert "No architecture data available" in result


class TestIsCacheStale:
    """Tests for _is_cache_stale."""

    def test_returns_none_when_no_cache_metadata(self):
        assert _is_cache_stale({}) is None
        assert _is_cache_stale({"project_id": 1}) is None

    def test_returns_none_when_no_last_synced(self):
        assert _is_cache_stale({"cache": {}}) is None
        assert _is_cache_stale({"cache": {"stale_after_hours": 24}}) is None

    def test_returns_false_when_fresh(self):
        now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
        config = {"cache": {"last_synced": now, "stale_after_hours": 24}}
        assert _is_cache_stale(config) is False

    def test_returns_true_when_stale(self):
        old = (datetime.now(timezone.utc) - timedelta(hours=48)).strftime("%Y-%m-%dT%H:%M:%SZ")
        config = {"cache": {"last_synced": old, "stale_after_hours": 24}}
        assert _is_cache_stale(config) is True

    def test_respects_custom_stale_after_hours(self):
        slightly_old = (datetime.now(timezone.utc) - timedelta(hours=2)).strftime("%Y-%m-%dT%H:%M:%SZ")
        # 1 hour threshold — 2 hours old is stale
        assert _is_cache_stale({"cache": {"last_synced": slightly_old, "stale_after_hours": 1}}) is True
        # 4 hour threshold — 2 hours old is fresh
        assert _is_cache_stale({"cache": {"last_synced": slightly_old, "stale_after_hours": 4}}) is False

    def test_defaults_to_24_hours(self):
        recent = (datetime.now(timezone.utc) - timedelta(hours=12)).strftime("%Y-%m-%dT%H:%M:%SZ")
        config = {"cache": {"last_synced": recent}}
        assert _is_cache_stale(config) is False

    def test_returns_true_on_invalid_timestamp(self):
        config = {"cache": {"last_synced": "not-a-date", "stale_after_hours": 24}}
        assert _is_cache_stale(config) is True


class TestFormatArchitectureEdgeCases:
    """Edge case tests for _format_architecture_txt."""

    def test_element_without_type(self):
        arch = {"elements": [{"name": "thing", "description": "A thing"}], "connections": [], "decisions": []}
        result = _format_architecture_txt(arch)
        # No brackets when type is empty
        assert "  thing — A thing" in result
        assert "[]" not in result

    def test_element_without_description(self):
        arch = {"elements": [{"name": "bare", "type": "svc"}], "connections": [], "decisions": []}
        result = _format_architecture_txt(arch)
        assert "[dim]\\[svc][/dim] bare" in result
        assert "—" not in result

    def test_connection_uses_description_fallback(self):
        arch = {"elements": [], "connections": [{"source": "a", "target": "b", "description": "data flow"}], "decisions": []}
        result = _format_architecture_txt(arch)
        assert "a -> b (data flow)" in result

    def test_connection_without_label_or_description(self):
        arch = {"elements": [], "connections": [{"source": "x", "target": "y"}], "decisions": []}
        result = _format_architecture_txt(arch)
        assert "x -> y" in result
        assert "()" not in result

    def test_connection_missing_source_target(self):
        arch = {"elements": [], "connections": [{}], "decisions": []}
        result = _format_architecture_txt(arch)
        assert "? -> ?" in result

    def test_decision_uses_name_fallback(self):
        arch = {"elements": [], "connections": [], "decisions": [{"name": "Use Redis", "status": "proposed"}]}
        result = _format_architecture_txt(arch)
        assert "Use Redis [proposed]" in result

    def test_decision_without_status(self):
        arch = {"elements": [], "connections": [], "decisions": [{"title": "Pick DB"}]}
        result = _format_architecture_txt(arch)
        assert "- Pick DB" in result
        assert "[]" not in result

    def test_elements_only_no_connections_no_decisions(self):
        arch = {"elements": [{"name": "solo"}]}
        result = _format_architecture_txt(arch)
        assert "solo" in result
        assert "[bold]Connections:[/bold]" not in result
        assert "[bold]Decisions:[/bold]" not in result


class TestFormatRulesEdgeCases:
    """Edge case tests for _format_rules_md."""

    def test_empty_rationale_falls_through_to_description(self):
        rules = [{"name": "r", "status": "active", "rationale": "", "description": "desc"}]
        result = _format_rules_md(rules)
        assert "desc" in result

    def test_both_rationale_and_description_empty(self):
        rules = [{"name": "r", "status": "active", "rationale": "", "description": ""}]
        result = _format_rules_md(rules)
        assert "**r** (active)" in result
        assert "—" not in result


class TestWriteCacheFiles:
    """Tests for write_cache_files."""

    def test_writes_all_three_files(self, tmp_path):
        gjalla_dir = tmp_path / ".gjalla"
        gjalla_dir.mkdir()
        (gjalla_dir / "config.yaml").write_text(yaml.dump({"project_id": 1}, default_flow_style=False))

        context = {
            "project_id": 1,
            "rules": [{"name": "r1", "status": "active"}],
            "architecture": {"elements": [{"name": "e1"}], "connections": [], "decisions": []},
        }
        write_cache_files(tmp_path, context)

        cache_dir = gjalla_dir / "cache"
        assert (cache_dir / "rules.md").exists()
        assert (cache_dir / "architecture.txt").exists()
        assert (cache_dir / "state.json").exists()

        state = json.loads((cache_dir / "state.json").read_text())
        assert state["project_id"] == 1

    def test_creates_cache_dir_if_missing(self, tmp_path):
        gjalla_dir = tmp_path / ".gjalla"
        gjalla_dir.mkdir()
        (gjalla_dir / "config.yaml").write_text(yaml.dump({"project_id": 1}, default_flow_style=False))

        write_cache_files(tmp_path, {"rules": [], "architecture": {}})
        assert (gjalla_dir / "cache").is_dir()

    def test_updates_config_with_cache_metadata(self, tmp_path):
        gjalla_dir = tmp_path / ".gjalla"
        gjalla_dir.mkdir()
        (gjalla_dir / "config.yaml").write_text(yaml.dump({"project_id": 42, "api_url": "https://x.io"}, default_flow_style=False))

        write_cache_files(tmp_path, {"rules": [], "architecture": {}})

        config = yaml.safe_load((gjalla_dir / "config.yaml").read_text())
        assert config["project_id"] == 42  # preserved
        assert config["api_url"] == "https://x.io"  # preserved
        assert "cache" in config
        assert "last_synced" in config["cache"]
        assert config["cache"]["stale_after_hours"] == 24

    def test_preserves_custom_stale_after_hours(self, tmp_path):
        gjalla_dir = tmp_path / ".gjalla"
        gjalla_dir.mkdir()
        (gjalla_dir / "config.yaml").write_text(yaml.dump({
            "project_id": 1,
            "cache": {"last_synced": "old", "stale_after_hours": 48},
        }, default_flow_style=False))

        write_cache_files(tmp_path, {"rules": [], "architecture": {}})

        config = yaml.safe_load((gjalla_dir / "config.yaml").read_text())
        assert config["cache"]["stale_after_hours"] == 48

    def test_handles_missing_config_yaml(self, tmp_path):
        gjalla_dir = tmp_path / ".gjalla"
        gjalla_dir.mkdir()
        # No config.yaml at all

        write_cache_files(tmp_path, {"rules": [], "architecture": {}})

        # Cache files should still be written
        assert (gjalla_dir / "cache" / "rules.md").exists()
        # config.yaml gets created with just cache metadata
        config = yaml.safe_load((gjalla_dir / "config.yaml").read_text())
        assert "cache" in config

    def test_handles_invalid_config_yaml(self, tmp_path):
        gjalla_dir = tmp_path / ".gjalla"
        gjalla_dir.mkdir()
        (gjalla_dir / "config.yaml").write_text("not: valid: yaml: {{{")

        write_cache_files(tmp_path, {"rules": [], "architecture": {}})

        # Should recover and write valid YAML config
        config = yaml.safe_load((gjalla_dir / "config.yaml").read_text())
        assert "cache" in config

    def test_handles_context_with_missing_keys(self, tmp_path):
        gjalla_dir = tmp_path / ".gjalla"
        gjalla_dir.mkdir()
        (gjalla_dir / "config.yaml").write_text(yaml.dump({"project_id": 1}, default_flow_style=False))

        # Context with no rules or architecture keys at all
        write_cache_files(tmp_path, {"project_id": 1})

        rules_content = (gjalla_dir / "cache" / "rules.md").read_text()
        assert "No rules defined" in rules_content
        arch_content = (gjalla_dir / "cache" / "architecture.txt").read_text()
        assert "No architecture data available" in arch_content
