"""Tests for src/fast_commands.py — fast (non-LLM) commands."""

import json
import pytest
from datetime import date, datetime
from pathlib import Path
from unittest.mock import patch, MagicMock

from src.fast_commands import (
    dispatch,
    cmd_commit,
    cmd_push,
    cmd_pull,
    cmd_test,
    cmd_email,
    cmd_calendar,
    cmd_note,
    cmd_jurnal,
    cmd_search,
    cmd_kb,
    cmd_remind,
    cmd_logs,
    cmd_doctor,
    cmd_heartbeat,
    cmd_help,
    COMMANDS,
    _auto_commit_message,
)


# --- Dispatch ---

class TestDispatch:
    def test_known_command(self):
        """Known commands return a string."""
        result = dispatch("help", [])
        assert result is not None
        assert "Comenzi disponibile" in result

    def test_unknown_command(self):
        """Unknown commands return None."""
        assert dispatch("nonexistent", []) is None

    def test_all_commands_registered(self):
        expected = {
            "commit", "push", "pull", "test", "email", "calendar",
            "note", "jurnal", "search", "kb", "remind", "logs",
            "doctor", "heartbeat", "help", "audio",
        }
        assert set(COMMANDS.keys()) == expected

    def test_handler_exception_caught(self):
        """Exceptions in handlers are caught and returned as error strings."""
        with patch.dict(COMMANDS, {"boom": lambda args: 1 / 0}):
            result = dispatch("boom", [])
            assert result is not None
            assert "Error in /boom" in result


# --- Git ---

class TestGitCommit:
    @patch("src.fast_commands._git")
    def test_nothing_to_commit(self, mock_git):
        mock_git.return_value = ""
        assert cmd_commit([]) == "Nothing to commit."

    @patch("src.fast_commands._git")
    def test_commit_with_custom_message(self, mock_git):
        mock_git.side_effect = [
            " M file.py\n",           # status --porcelain
            "",                         # add -A
            "",                         # commit -m
            "abc1234",                  # rev-parse --short HEAD
        ]
        result = cmd_commit(["fix", "bug"])
        assert "abc1234" in result
        assert "fix bug" in result
        assert "1 files" in result

    @patch("src.fast_commands._git")
    def test_commit_auto_message(self, mock_git):
        mock_git.side_effect = [
            " M src/router.py\n?? newfile.txt\n",  # status
            "",                                       # add -A
            "",                                       # commit -m
            "def5678",                                # rev-parse
        ]
        result = cmd_commit([])
        assert "def5678" in result
        assert "2 files" in result


class TestGitPush:
    @patch("src.fast_commands._git")
    def test_push_with_uncommitted(self, mock_git):
        mock_git.return_value = " M dirty.py\n"
        result = cmd_push([])
        assert "uncommitted" in result.lower()

    @patch("src.fast_commands._git")
    def test_push_up_to_date(self, mock_git):
        mock_git.side_effect = [
            "",                             # status --porcelain
            "Everything up-to-date",        # push
        ]
        result = cmd_push([])
        assert "up to date" in result.lower()

    @patch("src.fast_commands._git")
    def test_push_success(self, mock_git):
        mock_git.side_effect = [
            "",                             # status --porcelain
            "abc..def  master -> master",   # push
            "master",                       # branch --show-current
        ]
        result = cmd_push([])
        assert "origin/master" in result


class TestGitPull:
    @patch("src.fast_commands._git")
    def test_pull_up_to_date(self, mock_git):
        mock_git.return_value = "Already up to date."
        assert "up to date" in cmd_pull([]).lower()

    @patch("src.fast_commands._git")
    def test_pull_with_changes(self, mock_git):
        mock_git.return_value = "Fast-forward\n file.py | 2 +-"
        result = cmd_pull([])
        assert "Pulled" in result


# --- Test ---

class TestTestCmd:
    @patch("subprocess.run")
    def test_basic_run(self, mock_run):
        mock_run.return_value = MagicMock(
            stdout="5 passed in 1.2s",
            stderr="",
            returncode=0,
        )
        result = cmd_test([])
        assert "passed" in result

    @patch("subprocess.run")
    def test_with_pattern(self, mock_run):
        mock_run.return_value = MagicMock(
            stdout="2 passed",
            stderr="",
            returncode=0,
        )
        cmd_test(["router"])
        args = mock_run.call_args[0][0]
        assert "-k" in args
        assert "router" in args

    @patch("subprocess.run")
    def test_timeout(self, mock_run):
        import subprocess as sp
        mock_run.side_effect = sp.TimeoutExpired(cmd="pytest", timeout=120)
        result = cmd_test([])
        assert "timed out" in result.lower()


# --- Email ---

class TestEmail:
    @patch("subprocess.run")
    def test_email_check_clean(self, mock_run):
        mock_run.return_value = MagicMock(
            stdout=json.dumps({"ok": True, "unread_count": 0, "emails": []}),
            stderr="",
            returncode=0,
        )
        result = cmd_email([])
        assert "curat" in result.lower()

    @patch("subprocess.run")
    def test_email_check_unread(self, mock_run):
        mock_run.return_value = MagicMock(
            stdout=json.dumps({
                "ok": True,
                "unread_count": 2,
                "emails": [
                    {"subject": "Hello"},
                    {"subject": "Meeting"},
                ],
            }),
            stderr="",
            returncode=0,
        )
        result = cmd_email([])
        assert "2 necitite" in result
        assert "Hello" in result

    def test_email_unknown_sub(self):
        result = cmd_email(["foo"])
        assert "Unknown email sub-command" in result

    @patch("subprocess.run")
    def test_email_send(self, mock_run):
        mock_run.return_value = MagicMock(
            stdout=json.dumps({"ok": True}),
            stderr="",
            returncode=0,
        )
        result = cmd_email(["send", "test@x.com", "Subject", "::", "Body"])
        assert "trimis" in result.lower()

    def test_email_send_bad_format(self):
        result = cmd_email(["send", "no-separator"])
        assert "Format" in result

    @patch("subprocess.run")
    def test_email_save_nothing(self, mock_run):
        mock_run.return_value = MagicMock(
            stdout="Niciun email nou de la adrese whitelisted.",
            stderr="",
            returncode=0,
        )
        result = cmd_email(["save"])
        assert "Nimic de salvat" in result


# --- Calendar ---

class TestCalendar:
    @patch("subprocess.run")
    def test_calendar_today(self, mock_run):
        mock_run.return_value = MagicMock(
            stdout=json.dumps({
                "today": [{"time": "09:00", "summary": "Standup"}],
                "tomorrow": [],
            }),
            stderr="",
            returncode=0,
        )
        result = cmd_calendar([])
        assert "Standup" in result
        assert "09:00" in result

    @patch("subprocess.run")
    def test_calendar_week(self, mock_run):
        mock_run.return_value = MagicMock(
            stdout=json.dumps({
                "week_start": "10 Feb",
                "week_end": "16 Feb",
                "events": [{"date": "10 Feb", "time": "10:00", "summary": "Review"}],
            }),
            stderr="",
            returncode=0,
        )
        result = cmd_calendar(["week"])
        assert "Review" in result

    @patch("subprocess.run")
    def test_calendar_busy_free(self, mock_run):
        mock_run.return_value = MagicMock(
            stdout=json.dumps({"busy": False}),
            stderr="",
            returncode=0,
        )
        result = cmd_calendar(["busy"])
        assert "Liber" in result

    @patch("subprocess.run")
    def test_calendar_busy_occupied(self, mock_run):
        mock_run.return_value = MagicMock(
            stdout=json.dumps({"busy": True, "event": "Meeting", "ends": "16:30"}),
            stderr="",
            returncode=0,
        )
        result = cmd_calendar(["busy"])
        assert "Ocupat" in result
        assert "Meeting" in result

    def test_calendar_unknown_sub(self):
        result = cmd_calendar(["foo"])
        assert "Unknown calendar sub-command" in result


# --- Notes ---

class TestNote:
    def test_note_empty(self):
        assert "Usage" in cmd_note([])

    def test_note_creates_file(self, tmp_path):
        with patch("src.fast_commands.MEMORY_DIR", tmp_path):
            result = cmd_note(["test", "note"])
            assert result == "Notat."
            today = date.today().isoformat()
            filepath = tmp_path / f"{today}.md"
            assert filepath.exists()
            content = filepath.read_text()
            assert "test note" in content
            assert f"# {today}" in content

    def test_note_appends(self, tmp_path):
        today = date.today().isoformat()
        filepath = tmp_path / f"{today}.md"
        filepath.write_text(f"# {today}\n\n- existing\n")
        with patch("src.fast_commands.MEMORY_DIR", tmp_path):
            cmd_note(["new", "note"])
            content = filepath.read_text()
            assert "existing" in content
            assert "new note" in content


class TestJurnal:
    def test_jurnal_empty(self):
        assert "Usage" in cmd_jurnal([])

    def test_jurnal_creates_file(self, tmp_path):
        with patch("src.fast_commands.MEMORY_DIR", tmp_path):
            result = cmd_jurnal(["my", "entry"])
            assert result == "Jurnal actualizat."
            today = date.today().isoformat()
            filepath = tmp_path / f"{today}.md"
            content = filepath.read_text()
            assert "## Jurnal" in content
            assert "my entry" in content

    def test_jurnal_adds_section_if_missing(self, tmp_path):
        today = date.today().isoformat()
        filepath = tmp_path / f"{today}.md"
        filepath.write_text(f"# {today}\n\n- some note\n")
        with patch("src.fast_commands.MEMORY_DIR", tmp_path):
            cmd_jurnal(["entry"])
            content = filepath.read_text()
            assert "## Jurnal" in content
            assert "entry" in content


# --- Search ---

class TestSearch:
    def test_search_empty(self):
        assert "Usage" in cmd_search([])

    @patch("src.memory_search.search")
    def test_search_no_results(self, mock_search):
        mock_search.return_value = []
        result = cmd_search(["query"])
        assert "Niciun rezultat" in result

    @patch("src.memory_search.search")
    def test_search_with_results(self, mock_search):
        mock_search.return_value = [
            {"file": "notes.md", "chunk": "important info here", "score": 0.92},
        ]
        result = cmd_search(["important"])
        assert "Rezultate" in result
        assert "notes.md" in result
        assert "0.92" in result


# --- KB ---

class TestKB:
    def test_kb_no_index(self, tmp_path):
        with patch("src.fast_commands.MEMORY_DIR", tmp_path):
            result = cmd_kb([])
            assert "not found" in result.lower()

    def test_kb_list(self, tmp_path):
        kb_dir = tmp_path / "kb"
        kb_dir.mkdir()
        index = {
            "notes": [
                {"title": "Note 1", "date": "2026-02-15", "category": "insights"},
                {"title": "Note 2", "date": "2026-02-14", "category": "projects"},
            ]
        }
        (kb_dir / "index.json").write_text(json.dumps(index))
        with patch("src.fast_commands.MEMORY_DIR", tmp_path):
            result = cmd_kb([])
            assert "Note 1" in result
            assert "Note 2" in result

    def test_kb_filter_category(self, tmp_path):
        kb_dir = tmp_path / "kb"
        kb_dir.mkdir()
        index = {
            "notes": [
                {"title": "Note A", "date": "2026-02-15", "category": "insights"},
                {"title": "Note B", "date": "2026-02-14", "category": "projects"},
            ]
        }
        (kb_dir / "index.json").write_text(json.dumps(index))
        with patch("src.fast_commands.MEMORY_DIR", tmp_path):
            result = cmd_kb(["insights"])
            assert "Note A" in result
            assert "Note B" not in result

    def test_kb_filter_no_match(self, tmp_path):
        kb_dir = tmp_path / "kb"
        kb_dir.mkdir()
        index = {"notes": [{"title": "X", "date": "2026-01-01", "category": "a"}]}
        (kb_dir / "index.json").write_text(json.dumps(index))
        with patch("src.fast_commands.MEMORY_DIR", tmp_path):
            result = cmd_kb(["nonexistent"])
            assert "Nicio nota" in result


# --- Remind ---

class TestRemind:
    def test_remind_too_few_args(self):
        assert "Usage" in cmd_remind([])
        assert "Usage" in cmd_remind(["15:00"])

    def test_remind_bad_time(self):
        result = cmd_remind(["nottime", "test"])
        assert "Invalid time" in result

    @patch("src.fast_commands.TOOLS_DIR", Path("/tmp/fake_tools"))
    def test_remind_with_date(self):
        with patch.dict("sys.modules", {
            "calendar_check": MagicMock(
                create_event=MagicMock(return_value={"id": "123"})
            ),
        }):
            result = cmd_remind(["2026-03-01", "15:00", "Test", "reminder"])
            assert "Reminder setat" in result
            assert "15:00" in result


# --- Logs ---

class TestLogs:
    def test_logs_no_file(self, tmp_path):
        with patch("src.fast_commands.LOGS_DIR", tmp_path):
            result = cmd_logs([])
            assert "No log file" in result

    def test_logs_default(self, tmp_path):
        log_file = tmp_path / "echo-core.log"
        lines = [f"line {i}" for i in range(30)]
        log_file.write_text("\n".join(lines))
        with patch("src.fast_commands.LOGS_DIR", tmp_path):
            result = cmd_logs([])
            assert "line 29" in result
            assert "line 10" in result
            assert "line 0" not in result

    def test_logs_custom_count(self, tmp_path):
        log_file = tmp_path / "echo-core.log"
        lines = [f"line {i}" for i in range(30)]
        log_file.write_text("\n".join(lines))
        with patch("src.fast_commands.LOGS_DIR", tmp_path):
            result = cmd_logs(["5"])
            assert "line 29" in result
            assert "line 20" not in result


# --- Doctor ---

class TestDoctor:
    @patch("shutil.which", return_value="/usr/bin/claude")
    def test_doctor_runs(self, mock_which):
        result = cmd_doctor([])
        assert "Doctor:" in result
        assert "Claude CLI" in result


# --- Help ---

class TestHelp:
    def test_help_lists_commands(self):
        result = cmd_help([])
        assert "/commit" in result
        assert "/email" in result
        assert "/calendar" in result
        assert "/search" in result
        assert "/help" in result
        assert "/clear" in result


# --- Auto commit message ---

class TestAutoCommitMessage:
    def test_single_modified(self):
        msg = _auto_commit_message([" M src/router.py"])
        assert "src" in msg
        assert "~1" in msg

    def test_mixed_changes(self):
        files = [
            " M src/a.py",
            "?? tests/new.py",
            " D old.py",
        ]
        msg = _auto_commit_message(files)
        assert "+1" in msg
        assert "~1" in msg
        assert "-1" in msg

    def test_many_areas(self):
        files = [
            " M a/x.py",
            " M b/y.py",
            " M c/z.py",
            " M d/w.py",
        ]
        msg = _auto_commit_message(files)
        assert "+1 more" in msg
