# SPDX-License-Identifier: AGPL-3.0-only
# Copyright (c) 2026 sol pbc

import json
from pathlib import Path
from unittest.mock import patch

import pytest


def test_load_sync_state_missing(tmp_path):
    """Returns None when no state file exists."""
    from solstone.think.importers.sync import load_sync_state

    assert load_sync_state(tmp_path, "plaud") is None


def test_save_and_load_sync_state(tmp_path):
    """Round-trip: save then load."""
    from solstone.think.importers.sync import load_sync_state, save_sync_state

    state = {
        "backend": "plaud",
        "last_sync": "2026-02-14T12:00:00",
        "files": {
            "abc123": {
                "filename": "test.opus",
                "status": "imported",
            }
        },
    }
    save_sync_state(tmp_path, "plaud", state)
    loaded = load_sync_state(tmp_path, "plaud")
    assert loaded == state


def test_save_sync_state_creates_imports_dir(tmp_path):
    """imports/ dir is created if missing."""
    from solstone.think.importers.sync import save_sync_state

    save_sync_state(tmp_path, "plaud", {"backend": "plaud"})
    assert (tmp_path / "imports" / "plaud.json").exists()


def test_save_sync_state_no_temp_files(tmp_path):
    """No .tmp files left after save."""
    from solstone.think.importers.sync import save_sync_state

    save_sync_state(tmp_path, "plaud", {"backend": "plaud"})
    imports = tmp_path / "imports"
    temps = list(imports.glob("*.tmp"))
    assert temps == []


def test_load_sync_state_corrupt_json(tmp_path):
    """Returns None on corrupt JSON."""
    from solstone.think.importers.sync import load_sync_state

    state_path = tmp_path / "imports" / "plaud.json"
    state_path.parent.mkdir(parents=True)
    state_path.write_text("not json{{{", encoding="utf-8")
    assert load_sync_state(tmp_path, "plaud") is None


def test_get_syncable_backends():
    """Discovers plaud backend."""
    from solstone.think.importers.sync import get_syncable_backends

    backends = get_syncable_backends()
    names = [b.name for b in backends]
    assert "plaud" in names


def test_plaud_protocol_conformance():
    """PlaudBackend satisfies SyncableBackend protocol."""
    from solstone.think.importers.plaud import PlaudBackend
    from solstone.think.importers.sync import SyncableBackend

    assert isinstance(PlaudBackend(), SyncableBackend)


def test_plaud_sync_requires_token(tmp_path, monkeypatch):
    """Without token configured, PlaudBackend.sync() raises ValueError."""
    from solstone.think.importers.plaud import PlaudBackend

    monkeypatch.delenv("PLAUD_ACCESS_TOKEN", raising=False)
    with pytest.raises(ValueError, match="PLAUD_ACCESS_TOKEN"):
        PlaudBackend().sync(tmp_path)


def test_backends_cli_flag(capsys, monkeypatch):
    """sol import --backends lists plaud."""
    import sys

    from solstone.think.importers.cli import main

    monkeypatch.setattr(sys, "argv", ["sol import", "--backends"])
    monkeypatch.setenv("SOLSTONE_JOURNAL", "/tmp/test-journal")
    main()
    captured = capsys.readouterr()
    assert "plaud" in captured.out


# ---------------------------------------------------------------------------
# timestamp_from_start_time
# ---------------------------------------------------------------------------


def test_timestamp_from_start_time_seconds():
    """Handles epoch seconds."""
    # 2026-01-15 10:30:00 local
    import datetime as dt

    from solstone.think.importers.plaud import timestamp_from_start_time

    epoch = dt.datetime(2026, 1, 15, 10, 30, 0).timestamp()
    ts = timestamp_from_start_time(epoch)
    assert ts == "20260115_103000"


def test_timestamp_from_start_time_millis():
    """Handles epoch milliseconds (Plaud format)."""
    import datetime as dt

    from solstone.think.importers.plaud import timestamp_from_start_time

    epoch_ms = dt.datetime(2026, 1, 15, 10, 30, 0).timestamp() * 1000
    ts = timestamp_from_start_time(epoch_ms)
    assert ts == "20260115_103000"


# ---------------------------------------------------------------------------
# match_existing_imports
# ---------------------------------------------------------------------------


def _create_import(journal_root: Path, timestamp: str, original_filename: str) -> None:
    """Helper: create a minimal imports/{timestamp}/import.json."""
    import_dir = journal_root / "imports" / timestamp
    import_dir.mkdir(parents=True, exist_ok=True)
    meta = {"original_filename": original_filename}
    (import_dir / "import.json").write_text(json.dumps(meta), encoding="utf-8")


def test_match_exact_filename(tmp_path):
    """Matches by exact filename."""
    from solstone.think.importers.plaud import match_existing_imports

    _create_import(tmp_path, "20260115_103000", "Team Meeting.opus")
    plaud_files = [
        {"id": "abc", "filename": "Team Meeting", "fullname": "hash1.opus"},
    ]
    matches = match_existing_imports(tmp_path, plaud_files)
    assert matches == {"abc": "20260115_103000"}


def test_match_sanitized_filename(tmp_path):
    """Matches sanitized filename with extension."""
    from solstone.think.importers.plaud import match_existing_imports

    _create_import(tmp_path, "20260115_103000", "Team_Meeting.opus")
    plaud_files = [
        {"id": "abc", "filename": "Team Meeting", "fullname": "hash1.opus"},
    ]
    matches = match_existing_imports(tmp_path, plaud_files)
    assert matches == {"abc": "20260115_103000"}


def test_match_no_match(tmp_path):
    """Returns empty dict when no match."""
    from solstone.think.importers.plaud import match_existing_imports

    _create_import(tmp_path, "20260115_103000", "Something Else.m4a")
    plaud_files = [
        {"id": "abc", "filename": "Team Meeting", "fullname": "hash1.opus"},
    ]
    matches = match_existing_imports(tmp_path, plaud_files)
    assert matches == {}


def test_match_no_imports_dir(tmp_path):
    """Returns empty dict when imports/ doesn't exist."""
    from solstone.think.importers.plaud import match_existing_imports

    plaud_files = [
        {"id": "abc", "filename": "Team Meeting", "fullname": "hash1.opus"},
    ]
    matches = match_existing_imports(tmp_path, plaud_files)
    assert matches == {}


def test_match_by_stem(tmp_path):
    """Matches by filename stem (without extension)."""
    from solstone.think.importers.plaud import match_existing_imports

    _create_import(tmp_path, "20260115_103000", "Team Meeting.m4a")
    plaud_files = [
        {"id": "abc", "filename": "Team Meeting", "fullname": "hash1.opus"},
    ]
    matches = match_existing_imports(tmp_path, plaud_files)
    assert matches == {"abc": "20260115_103000"}


# ---------------------------------------------------------------------------
# PlaudBackend.sync() — catalog mode
# ---------------------------------------------------------------------------


def _mock_list_files(_session, _token):
    """Return a small fake Plaud file list."""
    return [
        {
            "id": "file1",
            "filename": "Standup",
            "fullname": "aaa.opus",
            "filesize": 5000,
            "start_time": 1737000000000,
        },
        {
            "id": "file2",
            "filename": "Retro",
            "fullname": "bbb.opus",
            "filesize": 8000,
            "start_time": 1737100000000,
        },
    ]


def test_plaud_sync_dry_run(tmp_path, monkeypatch):
    """Dry-run sync fetches catalog and saves state."""
    from solstone.think.importers.plaud import PlaudBackend
    from solstone.think.importers.sync import load_sync_state

    monkeypatch.setenv("PLAUD_ACCESS_TOKEN", "test-token")

    with patch(
        "solstone.think.importers.plaud.list_files", side_effect=_mock_list_files
    ):
        result = PlaudBackend().sync(tmp_path, dry_run=True)

    assert result["total"] == 2
    assert result["available"] == 2
    assert result["skipped"] == 0
    assert result["imported"] == 0
    assert result["downloaded"] == 0

    # State was saved
    state = load_sync_state(tmp_path, "plaud")
    assert state is not None
    assert len(state["files"]) == 2
    assert state["files"]["file1"]["status"] == "available"


def test_plaud_sync_matches_existing(tmp_path, monkeypatch):
    """Sync matches existing imports and marks them imported."""
    from solstone.think.importers.plaud import PlaudBackend
    from solstone.think.importers.sync import load_sync_state

    monkeypatch.setenv("PLAUD_ACCESS_TOKEN", "test-token")
    _create_import(tmp_path, "20260116_051320", "Standup.opus")

    with patch(
        "solstone.think.importers.plaud.list_files", side_effect=_mock_list_files
    ):
        result = PlaudBackend().sync(tmp_path, dry_run=True)

    assert result["imported"] == 1
    assert result["available"] == 1

    state = load_sync_state(tmp_path, "plaud")
    assert state["files"]["file1"]["status"] == "imported"
    assert state["files"]["file1"]["import_timestamp"] == "20260116_051320"
    assert state["files"]["file2"]["status"] == "available"


def test_plaud_sync_incremental(tmp_path, monkeypatch):
    """Second sync preserves existing state and detects new files."""
    from solstone.think.importers.plaud import PlaudBackend
    from solstone.think.importers.sync import load_sync_state, save_sync_state

    monkeypatch.setenv("PLAUD_ACCESS_TOKEN", "test-token")

    # Pre-seed state with file1 already imported
    save_sync_state(
        tmp_path,
        "plaud",
        {
            "backend": "plaud",
            "files": {
                "file1": {
                    "filename": "Standup",
                    "fullname": "aaa.opus",
                    "filesize": 5000,
                    "start_time": 1737000000000,
                    "status": "imported",
                    "import_timestamp": "20260116_051320",
                }
            },
        },
    )

    with patch(
        "solstone.think.importers.plaud.list_files", side_effect=_mock_list_files
    ):
        result = PlaudBackend().sync(tmp_path, dry_run=True)

    assert result["total"] == 2
    assert result["imported"] == 1
    assert result["available"] == 1

    state = load_sync_state(tmp_path, "plaud")
    # file1 preserved as imported
    assert state["files"]["file1"]["status"] == "imported"
    # file2 detected as new available
    assert state["files"]["file2"]["status"] == "available"


def test_plaud_sync_promotes_manually_imported(tmp_path, monkeypatch):
    """Available file gets promoted to imported if manually imported between syncs."""
    from solstone.think.importers.plaud import PlaudBackend
    from solstone.think.importers.sync import load_sync_state, save_sync_state

    monkeypatch.setenv("PLAUD_ACCESS_TOKEN", "test-token")

    # First sync: file2 is available
    save_sync_state(
        tmp_path,
        "plaud",
        {
            "backend": "plaud",
            "files": {
                "file1": {
                    "filename": "Standup",
                    "status": "imported",
                    "import_timestamp": "20260116_051320",
                },
                "file2": {
                    "filename": "Retro",
                    "fullname": "bbb.opus",
                    "filesize": 8000,
                    "start_time": 1737100000000,
                    "status": "available",
                },
            },
        },
    )

    # Simulate manual import of file2 (creates imports/*/import.json)
    _create_import(tmp_path, "20260117_134640", "Retro.opus")

    # Second sync: file2 should be promoted to imported
    with patch(
        "solstone.think.importers.plaud.list_files", side_effect=_mock_list_files
    ):
        result = PlaudBackend().sync(tmp_path, dry_run=True)

    assert result["imported"] == 2
    assert result["available"] == 0

    state = load_sync_state(tmp_path, "plaud")
    assert state["files"]["file2"]["status"] == "imported"
    assert state["files"]["file2"]["import_timestamp"] == "20260117_134640"


def _mock_list_files_with_junk(_session, _token):
    """Return a file list including trashed and short recordings."""
    return [
        {
            "id": "good1",
            "filename": "Team Standup",
            "fullname": "aaa.opus",
            "filesize": 5000,
            "start_time": 1737000000000,
            "duration": 300000,
            "is_trash": False,
        },
        {
            "id": "trashed1",
            "filename": "Old Recording",
            "fullname": "bbb.opus",
            "filesize": 2000,
            "start_time": 1737100000000,
            "duration": 60000,
            "is_trash": True,
        },
        {
            "id": "short1",
            "filename": "Accidental Tap",
            "fullname": "ccc.opus",
            "filesize": 500,
            "start_time": 1737200000000,
            "duration": 5000,
            "is_trash": False,
        },
    ]


def test_plaud_sync_skips_trashed_and_short(tmp_path, monkeypatch):
    """Trashed and short recordings are auto-skipped."""
    from solstone.think.importers.plaud import PlaudBackend
    from solstone.think.importers.sync import load_sync_state

    monkeypatch.setenv("PLAUD_ACCESS_TOKEN", "test-token")

    with patch(
        "solstone.think.importers.plaud.list_files",
        side_effect=_mock_list_files_with_junk,
    ):
        result = PlaudBackend().sync(tmp_path, dry_run=True)

    assert result["total"] == 3
    assert result["available"] == 1
    assert result["skipped"] == 2
    assert result["imported"] == 0

    state = load_sync_state(tmp_path, "plaud")
    assert state["files"]["good1"]["status"] == "available"
    assert state["files"]["trashed1"]["status"] == "skipped"
    assert state["files"]["trashed1"]["skip_reason"] == "trashed"
    assert state["files"]["short1"]["status"] == "skipped"
    assert state["files"]["short1"]["skip_reason"] == "too_short"


def test_plaud_sync_save_calls_import_one_in_process(tmp_path, monkeypatch):
    from solstone.think.importers.plaud import PlaudBackend
    from solstone.think.importers.sync import load_sync_state

    monkeypatch.setenv("PLAUD_ACCESS_TOKEN", "test-token")

    def fake_download(_session, _url, dest_path, progress_cb=None):
        dest_path.write_bytes(b"audio")
        if progress_cb:
            progress_cb()
        return True

    with (
        patch(
            "solstone.think.importers.plaud.list_files", side_effect=_mock_list_files
        ),
        patch(
            "solstone.think.importers.plaud.get_temp_url", return_value="https://temp"
        ),
        patch(
            "solstone.think.importers.plaud.download_to_file", side_effect=fake_download
        ),
        patch("solstone.think.importers.plaud.import_one") as import_mock,
    ):
        result = PlaudBackend().sync(tmp_path, dry_run=False)

    assert result["downloaded"] == 2
    assert import_mock.call_count == 2
    for call in import_mock.call_args_list:
        assert call.kwargs["source"] == "plaud"
        assert call.kwargs["auto"] is True
        assert call.kwargs["timestamp"]
        assert call.kwargs["wait_for_processing"] is False

    state = load_sync_state(tmp_path, "plaud")
    assert state["files"]["file1"]["status"] == "imported"
    assert state["files"]["file2"]["status"] == "imported"


def test_plaud_sync_checkpoints_catalog_per_file(tmp_path, monkeypatch):
    from solstone.think.importers.plaud import PlaudBackend

    monkeypatch.setenv("PLAUD_ACCESS_TOKEN", "test-token")
    saved_states = []

    def fake_download(_session, _url, dest_path, progress_cb=None):
        dest_path.write_bytes(b"audio")
        if progress_cb:
            progress_cb()
        return True

    def fake_save_sync_state(_journal_root, _backend, state):
        saved_states.append(json.loads(json.dumps(state)))

    with (
        patch(
            "solstone.think.importers.plaud.list_files", side_effect=_mock_list_files
        ),
        patch(
            "solstone.think.importers.plaud.get_temp_url", return_value="https://temp"
        ),
        patch(
            "solstone.think.importers.plaud.download_to_file", side_effect=fake_download
        ),
        patch(
            "solstone.think.importers.plaud.import_one",
            return_value={"segments": ["seg"]},
        ),
        patch(
            "solstone.think.importers.sync.save_sync_state",
            side_effect=fake_save_sync_state,
        ) as save_mock,
    ):
        result = PlaudBackend().sync(tmp_path, dry_run=False)

    assert result["downloaded"] == 2
    assert save_mock.call_count == 3
    per_iteration_states = saved_states[:-1]
    assert any(
        any(info.get("status") == "imported" for info in state["files"].values())
        for state in per_iteration_states
    )


def test_plaud_sync_save_records_import_one_errors(tmp_path, monkeypatch):
    from solstone.think.importers.plaud import PlaudBackend

    monkeypatch.setenv("PLAUD_ACCESS_TOKEN", "test-token")

    def fake_download(_session, _url, dest_path, progress_cb=None):
        dest_path.write_bytes(b"audio")
        return True

    with (
        patch(
            "solstone.think.importers.plaud.list_files", side_effect=_mock_list_files
        ),
        patch(
            "solstone.think.importers.plaud.get_temp_url", return_value="https://temp"
        ),
        patch(
            "solstone.think.importers.plaud.download_to_file", side_effect=fake_download
        ),
        patch(
            "solstone.think.importers.plaud.import_one",
            side_effect=RuntimeError("import boom"),
        ),
    ):
        result = PlaudBackend().sync(tmp_path, dry_run=False)

    assert result["downloaded"] == 0
    assert len(result["errors"]) == 2
    assert all("import boom" in error for error in result["errors"])


@pytest.mark.parametrize(
    ("flags", "expected_dry_run"),
    [
        ([], True),
        (["--save"], False),
        (["--dry-run"], True),
        (["--save", "--dry-run"], True),
    ],
)
def test_sync_cli_dry_run_flag_matrix(flags, expected_dry_run, monkeypatch, tmp_path):
    """--dry-run is authoritative for sync dispatch dry-run behavior."""
    import sys

    from solstone.think.importers.cli import main

    monkeypatch.setattr(sys, "argv", ["sol import", "--sync", "plaud", *flags])
    monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))

    with patch("solstone.think.importers.cli._run_sync") as mock_run:
        main()

    assert mock_run.call_args.kwargs["dry_run"] is expected_dry_run


def test_plaud_sync_cli_flag(capsys, monkeypatch, tmp_path):
    """sol import --sync plaud runs sync in dry-run mode."""
    import sys

    from solstone.think.importers.cli import main

    monkeypatch.setattr(sys, "argv", ["sol import", "--sync", "plaud"])
    monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path))
    monkeypatch.setenv("PLAUD_ACCESS_TOKEN", "test-token")

    with patch(
        "solstone.think.importers.plaud.list_files", side_effect=_mock_list_files
    ):
        main()

    captured = capsys.readouterr()
    assert "Total:" in captured.out
    assert "Available to import:" in captured.out
