"""Tests for the Tyko client."""

import io
import os
import time
from unittest.mock import MagicMock, patch

import pytest
from pytest_httpx import HTTPXMock

from tyko import Experiment, Project, Run, TykoClient
from tyko._sidecar import stop_sentinel_path
from tyko._types import TykoLogHandler, _TeeWriter


@pytest.fixture(autouse=True)
def default_api_key():
    """Set default environment variables for tests."""
    with patch.dict("os.environ", {"TYKO_API_KEY": "test-api-key"}):
        yield "test-api-key"


def test_client_uses_default_endpoint(default_api_key: str):
    """Test that the client uses the default API endpoint."""
    client = TykoClient()
    assert client.api_url == "https://api.tyko-labs.com"
    assert client.api_key == default_api_key


def test_client_uses_env_vars():
    """Test that the client uses environment variables for configuration."""
    with patch.dict("os.environ", {"TYKO_API_URL": "http://localhost:8000"}):
        client = TykoClient()
        assert client.api_url == "http://localhost:8000"


def test_client_without_api_key():
    """Client can be created without an API key (for anonymous access)."""
    with patch.dict("os.environ", {}, clear=True):
        client = TykoClient(api_url="http://localhost:8080")
        assert client.api_key is None
        assert client.api_url == "http://localhost:8080"


def test_start_run_with_project(httpx_mock: HTTPXMock):
    """Test starting a run with just a project name."""

    httpx_mock.add_response(
        method="POST",
        url="https://api.tyko-labs.com/runs",
        json={
            "id": "run-id-1234",
            "project": {"name": "my-project"},
            "experiment": {"name": "default"},
            "name": "run-1234",
            "sequential": 1,
        },
    )
    # PATCH for setting status to "running" on __enter__
    httpx_mock.add_response(
        method="PATCH",
        url="https://api.tyko-labs.com/runs/run-id-1234",
        json={},
    )
    # PATCH for setting status to "completed" on __exit__
    httpx_mock.add_response(
        method="PATCH",
        url="https://api.tyko-labs.com/runs/run-id-1234",
        json={},
    )

    client = TykoClient()

    with client.start_run(project="my-project") as run:
        assert isinstance(run, Run)
        assert isinstance(run.experiment, Experiment)
        assert isinstance(run.project, Project)
        assert run.project.name == "my-project"
        assert run.experiment.name == "default"
        assert run.name == "run-1234"

    requests = httpx_mock.get_requests()

    assert len(requests) == 3
    assert requests[0].url == "https://api.tyko-labs.com/runs"
    assert requests[0].headers["Authorization"] == "Bearer test-api-key"

    # Verify environment is automatically captured and sent
    import json

    create_body = json.loads(requests[0].content)
    assert "environment" in create_body
    assert "python_version" in create_body["environment"]
    assert "platform" in create_body["environment"]
    assert "cpu_count" in create_body["environment"]

    # Verify status is set to "running" on enter
    running_body = json.loads(requests[1].content)
    assert running_body["status"] == "running"


def test_run_closes_with_completed_status(httpx_mock: HTTPXMock):
    """Test that a run sends 'completed' status when exiting without exception."""
    httpx_mock.add_response(
        method="POST",
        url="https://api.tyko-labs.com/runs",
        json={
            "id": "run-id-5678",
            "project": {"name": "test-project"},
            "experiment": {"name": "default"},
            "name": "test-run",
            "sequential": 1,
        },
    )
    # PATCH for "running" status
    httpx_mock.add_response(
        method="PATCH",
        url="https://api.tyko-labs.com/runs/run-id-5678",
        json={},
    )
    # PATCH for "completed" status
    httpx_mock.add_response(
        method="PATCH",
        url="https://api.tyko-labs.com/runs/run-id-5678",
        json={},
    )

    client = TykoClient()

    with client.start_run(project="test-project"):
        pass

    requests = httpx_mock.get_requests()

    # First PATCH sets status to "running"
    running_request = requests[1]
    assert running_request.method == "PATCH"

    import json

    running_body = json.loads(running_request.content)
    assert running_body["status"] == "running"

    # Second PATCH sets status to "completed"
    completed_request = requests[2]
    assert completed_request.method == "PATCH"
    assert completed_request.url == "https://api.tyko-labs.com/runs/run-id-5678"

    completed_body = json.loads(completed_request.content)
    assert completed_body["status"] == "completed"
    assert "at" in completed_body


def test_run_closes_with_failed_status_on_exception(httpx_mock: HTTPXMock):
    """Test that a run sends 'failed' status when exiting with an exception."""
    httpx_mock.add_response(
        method="POST",
        url="https://api.tyko-labs.com/runs",
        json={
            "id": "run-id-9999",
            "project": {"name": "test-project"},
            "experiment": {"name": "default"},
            "name": "failed-run",
            "sequential": 2,
        },
    )
    # PATCH for "running" status
    httpx_mock.add_response(
        method="PATCH",
        url="https://api.tyko-labs.com/runs/run-id-9999",
        json={},
    )
    # PATCH for "failed" status
    httpx_mock.add_response(
        method="PATCH",
        url="https://api.tyko-labs.com/runs/run-id-9999",
        json={},
    )

    client = TykoClient()

    with pytest.raises(ValueError):
        with client.start_run(project="test-project"):
            raise ValueError("Something went wrong")

    requests = httpx_mock.get_requests()

    # Second PATCH sets status to "failed"
    failed_request = requests[2]
    assert failed_request.method == "PATCH"
    assert failed_request.url == "https://api.tyko-labs.com/runs/run-id-9999"

    import json

    body = json.loads(failed_request.content)
    assert body["status"] == "failed"
    assert "at" in body


def test_run_params_update(httpx_mock: HTTPXMock):
    """Test setting multiple config values with update()."""
    httpx_mock.add_response(
        method="POST",
        url="https://api.tyko-labs.com/runs",
        json={
            "id": "run-config-test",
            "project": {"name": "test-project"},
            "experiment": {"name": "default"},
            "name": "config-run",
            "sequential": 1,
        },
    )
    # PATCH for "running" status
    httpx_mock.add_response(
        method="PATCH",
        url="https://api.tyko-labs.com/runs/run-config-test",
        json={},
    )
    # PATCH for params update
    httpx_mock.add_response(
        method="PATCH",
        url="https://api.tyko-labs.com/runs/run-config-test",
        json={},
    )
    # PATCH for "completed" status
    httpx_mock.add_response(
        method="PATCH",
        url="https://api.tyko-labs.com/runs/run-config-test",
        json={},
    )

    client = TykoClient()

    with client.start_run(project="test-project") as run:
        run.params.update({"learning_rate": 0.001, "batch_size": 32})

    requests = httpx_mock.get_requests()

    # First request is POST to create run
    assert requests[0].method == "POST"

    # Second request is PATCH to set status to "running"
    assert requests[1].method == "PATCH"

    import json

    running_body = json.loads(requests[1].content)
    assert running_body["status"] == "running"

    # Third request is PATCH to set config
    assert requests[2].method == "PATCH"
    assert requests[2].url == "https://api.tyko-labs.com/runs/run-config-test"

    config_body = json.loads(requests[2].content)
    assert config_body["params"] == {"learning_rate": 0.001, "batch_size": 32}

    # Fourth request is PATCH to close run
    assert requests[3].method == "PATCH"
    close_body = json.loads(requests[3].content)
    assert close_body["status"] == "completed"


def test_run_params_setitem(httpx_mock: HTTPXMock):
    """Test setting config values with dict-style access."""
    httpx_mock.add_response(
        method="POST",
        url="https://api.tyko-labs.com/runs",
        json={
            "id": "run-setitem-test",
            "project": {"name": "test-project"},
            "experiment": {"name": "default"},
            "name": "setitem-run",
            "sequential": 1,
        },
    )
    # PATCH for "running" status + two for config sets + one for close
    httpx_mock.add_response(
        method="PATCH",
        url="https://api.tyko-labs.com/runs/run-setitem-test",
        json={},
    )
    httpx_mock.add_response(
        method="PATCH",
        url="https://api.tyko-labs.com/runs/run-setitem-test",
        json={},
    )
    httpx_mock.add_response(
        method="PATCH",
        url="https://api.tyko-labs.com/runs/run-setitem-test",
        json={},
    )
    httpx_mock.add_response(
        method="PATCH",
        url="https://api.tyko-labs.com/runs/run-setitem-test",
        json={},
    )

    client = TykoClient()

    with client.start_run(project="test-project") as run:
        run.params["learning_rate"] = 0.001
        run.params["batch_size"] = 32

    requests = httpx_mock.get_requests()

    import json

    # Second request sets status to "running"
    running_body = json.loads(requests[1].content)
    assert running_body["status"] == "running"

    # Third request sets learning_rate
    config_body_1 = json.loads(requests[2].content)
    assert config_body_1["params"] == {"learning_rate": 0.001}

    # Fourth request sets batch_size
    config_body_2 = json.loads(requests[3].content)
    assert config_body_2["params"] == {"batch_size": 32}


def test_run_params_with_various_types(httpx_mock: HTTPXMock):
    """Test setting config with bool, int, float, and string values."""
    httpx_mock.add_response(
        method="POST",
        url="https://api.tyko-labs.com/runs",
        json={
            "id": "run-types-test",
            "project": {"name": "test-project"},
            "experiment": {"name": "default"},
            "name": "types-run",
            "sequential": 1,
        },
    )
    # PATCH for "running" status
    httpx_mock.add_response(
        method="PATCH",
        url="https://api.tyko-labs.com/runs/run-types-test",
        json={},
    )
    # PATCH for params update
    httpx_mock.add_response(
        method="PATCH",
        url="https://api.tyko-labs.com/runs/run-types-test",
        json={},
    )
    # PATCH for "completed" status
    httpx_mock.add_response(
        method="PATCH",
        url="https://api.tyko-labs.com/runs/run-types-test",
        json={},
    )

    client = TykoClient()

    config: dict[str, object] = {
        "enabled": True,
        "epochs": 100,
        "learning_rate": 0.001,
        "model_name": "transformer",
    }

    with client.start_run(project="test-project") as run:
        run.params.update(config)

    requests = httpx_mock.get_requests()

    import json

    # Second request is "running" status, third is params
    config_body = json.loads(requests[2].content)
    assert config_body["params"]["enabled"] is True
    assert config_body["params"]["epochs"] == 100
    assert config_body["params"]["learning_rate"] == 0.001
    assert config_body["params"]["model_name"] == "transformer"


def test_run_params_local_cache(httpx_mock: HTTPXMock):
    """Test that config values are cached locally for reading."""
    httpx_mock.add_response(
        method="POST",
        url="https://api.tyko-labs.com/runs",
        json={
            "id": "run-cache-test",
            "project": {"name": "test-project"},
            "experiment": {"name": "default"},
            "name": "cache-run",
            "sequential": 1,
        },
    )
    # PATCH for "running" status
    httpx_mock.add_response(
        method="PATCH",
        url="https://api.tyko-labs.com/runs/run-cache-test",
        json={},
    )
    # PATCH for params update
    httpx_mock.add_response(
        method="PATCH",
        url="https://api.tyko-labs.com/runs/run-cache-test",
        json={},
    )
    # PATCH for "completed" status
    httpx_mock.add_response(
        method="PATCH",
        url="https://api.tyko-labs.com/runs/run-cache-test",
        json={},
    )

    client = TykoClient()

    with client.start_run(project="test-project") as run:
        run.params["learning_rate"] = 0.001
        # Should be able to read back the value from local cache
        assert run.params["learning_rate"] == 0.001
        assert "learning_rate" in run.params
        assert len(run.params) == 1


def test_run_log_method(httpx_mock: HTTPXMock):
    """Test the run.log() method for logging metrics."""
    httpx_mock.add_response(
        method="POST",
        url="https://api.tyko-labs.com/runs",
        json={
            "id": "run-id-log-test",
            "project": {"name": "test-project"},
            "experiment": {"name": "default"},
            "name": "test-run",
            "sequential": 1,
        },
    )
    # PATCH for "running" status
    httpx_mock.add_response(
        method="PATCH",
        url="https://api.tyko-labs.com/runs/run-id-log-test",
        json={},
    )
    # Allow the entries endpoint to be called multiple times
    httpx_mock.add_response(
        method="POST",
        url="https://api.tyko-labs.com/runs/run-id-log-test/entries",
        json={"status": "success"},
    )
    httpx_mock.add_response(
        method="POST",
        url="https://api.tyko-labs.com/runs/run-id-log-test/entries",
        json={"status": "success"},
    )
    # PATCH for "completed" status
    httpx_mock.add_response(
        method="PATCH",
        url="https://api.tyko-labs.com/runs/run-id-log-test",
        json={},
    )

    client = TykoClient()

    with client.start_run(project="test-project") as run:
        # Log some metrics
        run.log({"loss": 0.5, "accuracy": 0.8})
        time.sleep(0.01)  # Small delay to ensure different timestamps
        run.log({"loss": 0.4, "accuracy": 0.85})

    requests = httpx_mock.get_requests()

    # Should have: POST /runs, PATCH (running), POST entries (x2), PATCH (completed)
    assert len(requests) == 5

    # Check the first log entries request (after running status PATCH)
    log_request1 = requests[2]
    assert log_request1.url == "https://api.tyko-labs.com/runs/run-id-log-test/entries"
    assert log_request1.method == "POST"

    import json

    log_body1 = json.loads(log_request1.content)
    assert "entries" in log_body1
    assert len(log_body1["entries"]) == 1

    # Check first entry
    entry1 = log_body1["entries"][0]
    assert "timestamp" in entry1
    assert "values" in entry1
    assert entry1["values"] == {"loss": 0.5, "accuracy": 0.8}

    # Check the second log entries request
    log_request2 = requests[3]
    log_body2 = json.loads(log_request2.content)
    entry2 = log_body2["entries"][0]
    assert entry2["values"] == {"loss": 0.4, "accuracy": 0.85}

    # Timestamps should be different
    assert entry1["timestamp"] != entry2["timestamp"]


def test_run_log_with_grouped_metrics(httpx_mock: HTTPXMock):
    """Test logging metrics with grouped names (train/, eval/)."""
    httpx_mock.add_response(
        method="POST",
        url="https://api.tyko-labs.com/runs",
        json={
            "id": "run-id-grouped",
            "project": {"name": "test-project"},
            "experiment": {"name": "default"},
            "name": "test-run",
            "sequential": 1,
        },
    )
    # PATCH for "running" status
    httpx_mock.add_response(
        method="PATCH",
        url="https://api.tyko-labs.com/runs/run-id-grouped",
        json={},
    )
    httpx_mock.add_response(
        method="POST",
        url="https://api.tyko-labs.com/runs/run-id-grouped/entries",
        json={"status": "success"},
    )
    # PATCH for "completed" status
    httpx_mock.add_response(
        method="PATCH",
        url="https://api.tyko-labs.com/runs/run-id-grouped",
        json={},
    )

    client = TykoClient()

    with client.start_run(project="test-project") as run:
        # Log grouped metrics
        run.log(
            {
                "train/loss": 0.5,
                "train/accuracy": 0.8,
                "eval/loss": 0.6,
                "eval/accuracy": 0.75,
            }
        )

    requests = httpx_mock.get_requests()
    # Log request is now at index 2 (after POST /runs and PATCH running)
    log_request = requests[2]

    import json

    log_body = json.loads(log_request.content)
    entry = log_body["entries"][0]

    assert "train/loss" in entry["values"]
    assert "train/accuracy" in entry["values"]
    assert "eval/loss" in entry["values"]
    assert "eval/accuracy" in entry["values"]


def test_run_log_multiple_calls(httpx_mock: HTTPXMock):
    """Test that multiple log calls work correctly."""
    httpx_mock.add_response(
        method="POST",
        url="https://api.tyko-labs.com/runs",
        json={
            "id": "run-id-batch",
            "project": {"name": "test-project"},
            "experiment": {"name": "default"},
            "name": "test-run",
            "sequential": 1,
        },
    )
    # PATCH for "running" status
    httpx_mock.add_response(
        method="PATCH",
        url="https://api.tyko-labs.com/runs/run-id-batch",
        json={},
    )
    # Add responses for 5 log calls
    for _ in range(5):
        httpx_mock.add_response(
            method="POST",
            url="https://api.tyko-labs.com/runs/run-id-batch/entries",
            json={"status": "success"},
        )
    # PATCH for "completed" status
    httpx_mock.add_response(
        method="PATCH",
        url="https://api.tyko-labs.com/runs/run-id-batch",
        json={},
    )

    client = TykoClient()

    with client.start_run(project="test-project") as run:
        # Multiple log calls
        for i in range(5):
            run.log({"step": i, "loss": 1.0 / (i + 1)})

    requests = httpx_mock.get_requests()

    # Should have: 1 POST /runs + 1 PATCH (running) + 5 POST /entries + 1 PATCH (completed) = 8 requests
    assert len(requests) == 8

    # Verify the entries were logged with correct values
    for i in range(5):
        log_request = requests[i + 2]  # Skip POST /runs and PATCH (running)

        import json

        log_body = json.loads(log_request.content)
        assert len(log_body["entries"]) == 1

        entry = log_body["entries"][0]
        assert entry["values"]["step"] == i
        assert entry["values"]["loss"] == pytest.approx(1.0 / (i + 1))


def test_run_should_stop_returns_false_by_default(httpx_mock: HTTPXMock):
    """Test that should_stop() returns False when no sentinel file exists."""
    httpx_mock.add_response(
        method="POST",
        url="https://api.tyko-labs.com/runs",
        json={
            "id": "run-stop-test",
            "project": {"name": "test-project"},
            "experiment": {"name": "default"},
            "name": "stop-run",
            "sequential": 1,
        },
    )
    httpx_mock.add_response(method="PATCH", url="https://api.tyko-labs.com/runs/run-stop-test", json={})
    httpx_mock.add_response(method="PATCH", url="https://api.tyko-labs.com/runs/run-stop-test", json={})

    client = TykoClient()

    with client.start_run(project="test-project") as run:
        assert run.should_stop() is False


def test_run_should_stop_returns_true_when_sentinel_exists(httpx_mock: HTTPXMock):
    """Test that should_stop() returns True when the sentinel file exists."""
    httpx_mock.add_response(
        method="POST",
        url="https://api.tyko-labs.com/runs",
        json={
            "id": "run-stop-test-2",
            "project": {"name": "test-project"},
            "experiment": {"name": "default"},
            "name": "stop-run-2",
            "sequential": 1,
        },
    )
    httpx_mock.add_response(method="PATCH", url="https://api.tyko-labs.com/runs/run-stop-test-2", json={})
    httpx_mock.add_response(method="PATCH", url="https://api.tyko-labs.com/runs/run-stop-test-2", json={})

    client = TykoClient()

    with client.start_run(project="test-project") as run:
        assert run.should_stop() is False
        # Simulate sidecar writing the sentinel file
        sentinel = stop_sentinel_path(client_uuid=client._client_uuid)
        with open(sentinel, "w") as f:
            f.write("stop")
        assert run.should_stop() is True


def test_run_closes_with_stopped_status_when_sentinel_exists(httpx_mock: HTTPXMock):
    """Test that a run sends 'stopped' status on exit when sentinel file exists."""
    httpx_mock.add_response(
        method="POST",
        url="https://api.tyko-labs.com/runs",
        json={
            "id": "run-stopped-exit",
            "project": {"name": "test-project"},
            "experiment": {"name": "default"},
            "name": "stopped-run",
            "sequential": 1,
        },
    )
    httpx_mock.add_response(method="PATCH", url="https://api.tyko-labs.com/runs/run-stopped-exit", json={})
    httpx_mock.add_response(method="PATCH", url="https://api.tyko-labs.com/runs/run-stopped-exit", json={})

    client = TykoClient()

    with client.start_run(project="test-project"):
        sentinel = stop_sentinel_path(client_uuid=client._client_uuid)
        with open(sentinel, "w") as f:
            f.write("stop")

    requests = httpx_mock.get_requests()

    import json

    close_body = json.loads(requests[2].content)
    assert close_body["status"] == "stopped"
    assert "at" in close_body

    # Sentinel file should be cleaned up after exit
    assert not os.path.exists(sentinel)


def test_sentinel_file_cleaned_up_on_exit(httpx_mock: HTTPXMock):
    """Test that the sentinel file is removed on run exit."""
    httpx_mock.add_response(
        method="POST",
        url="https://api.tyko-labs.com/runs",
        json={
            "id": "run-sentinel-cleanup",
            "project": {"name": "test-project"},
            "experiment": {"name": "default"},
            "name": "sentinel-run",
            "sequential": 1,
        },
    )
    httpx_mock.add_response(method="PATCH", url="https://api.tyko-labs.com/runs/run-sentinel-cleanup", json={})
    httpx_mock.add_response(method="PATCH", url="https://api.tyko-labs.com/runs/run-sentinel-cleanup", json={})

    client = TykoClient()
    sentinel = stop_sentinel_path(client_uuid=client._client_uuid)

    with client.start_run(project="test-project"):
        with open(sentinel, "w") as f:
            f.write("stop")
        assert os.path.exists(sentinel)

    assert not os.path.exists(sentinel)


# def test_start_run_with_experiment():
#     """Test starting a run with project and experiment names."""
#     client = TykoClient()
#     with client.start_run(project="my-project", experiment="baseline") as run:
#         assert run.project.name == "my-project"
#         assert run.experiment.name == "baseline"


# ---------------------------------------------------------------------------
# _TeeWriter tests
# ---------------------------------------------------------------------------


def _make_tee(key: str = "console/stdout") -> tuple[_TeeWriter, io.StringIO, MagicMock]:
    """Helper: create a _TeeWriter with a StringIO original and a mock IPC."""
    original = io.StringIO()
    ipc = MagicMock()
    tee = _TeeWriter(original, key, ipc, start_time_ns=0)
    return tee, original, ipc


def test_tee_writer_writes_through_to_original():
    """_TeeWriter writes to the original stream."""
    tee, original, _ = _make_tee()
    tee.write("hello\n")
    assert original.getvalue() == "hello\n"


def test_tee_writer_sends_complete_line_via_ipc():
    """_TeeWriter sends a complete line via IPC when newline is received."""
    tee, _, ipc = _make_tee()
    tee.write("hello\n")
    assert ipc.send.call_count == 1
    payload = ipc.send.call_args[0][0]
    assert payload["type"] == "log"
    assert payload["values"]["console/stdout"] == "hello"


def test_tee_writer_buffers_partial_lines():
    """_TeeWriter buffers partial writes and sends only on newline."""
    tee, _, ipc = _make_tee()
    tee.write("hel")
    assert ipc.send.call_count == 0
    tee.write("lo\n")
    assert ipc.send.call_count == 1


def test_tee_writer_multiple_lines_in_one_write():
    """_TeeWriter handles multiple newlines in a single write."""
    tee, _, ipc = _make_tee()
    tee.write("line1\nline2\nline3\n")
    assert ipc.send.call_count == 3
    texts = [call[0][0]["values"]["console/stdout"] for call in ipc.send.call_args_list]
    assert texts == ["line1", "line2", "line3"]


def test_tee_writer_flush_sends_partial_buffer():
    """flush() sends any buffered partial line."""
    tee, _, ipc = _make_tee()
    tee.write("no newline")
    assert ipc.send.call_count == 0
    tee.flush()
    assert ipc.send.call_count == 1
    assert ipc.send.call_args[0][0]["values"]["console/stdout"] == "no newline"


def test_tee_writer_flush_empty_buffer_does_nothing():
    """flush() is a no-op when buffer is empty."""
    tee, _, ipc = _make_tee()
    tee.flush()
    assert ipc.send.call_count == 0


def test_tee_writer_stderr_key():
    """_TeeWriter uses the given key for stderr."""
    tee, _, ipc = _make_tee(key="console/stderr")
    tee.write("error!\n")
    assert ipc.send.call_args[0][0]["values"]["console/stderr"] == "error!"


def test_tee_writer_delegates_unknown_attrs():
    """_TeeWriter delegates unknown attributes to the original stream."""
    tee, original, _ = _make_tee()
    # encoding is delegated to the StringIO (which has it)
    assert tee.encoding == original.encoding


# ---------------------------------------------------------------------------
# TykoLogHandler tests
# ---------------------------------------------------------------------------


def test_tyko_log_handler_sends_formatted_record():
    """TykoLogHandler sends formatted log records via IPC."""
    import logging

    ipc = MagicMock()
    handler = TykoLogHandler(ipc=ipc, start_time_ns=0)
    handler.setFormatter(logging.Formatter("%(levelname)s %(message)s"))

    record = logging.LogRecord(
        name="test",
        level=logging.INFO,
        pathname="",
        lineno=0,
        msg="hello world",
        args=(),
        exc_info=None,
    )
    handler.emit(record)

    assert ipc.send.call_count == 1
    payload = ipc.send.call_args[0][0]
    assert payload["type"] == "log"
    assert "hello world" in payload["values"]["console/stdout"]


def test_tyko_log_handler_swallows_send_errors():
    """TykoLogHandler does not raise if IPC send fails."""
    import logging

    ipc = MagicMock()
    ipc.send.side_effect = RuntimeError("broken pipe")
    handler = TykoLogHandler(ipc=ipc, start_time_ns=0)

    record = logging.LogRecord(
        name="test",
        level=logging.WARNING,
        pathname="",
        lineno=0,
        msg="oops",
        args=(),
        exc_info=None,
    )
    # Should not raise
    handler.emit(record)


# ---------------------------------------------------------------------------
# Console capture integration (no IPC → no tee installed)
# ---------------------------------------------------------------------------


def test_capture_output_false_does_not_install_tee(httpx_mock: HTTPXMock):
    """capture_output=False skips tee installation even when IPC is available."""
    httpx_mock.add_response(
        method="POST",
        url="https://api.tyko-labs.com/runs",
        json={"id": "r1", "project": {"name": "p"}, "experiment": {"name": "e"}, "name": "run", "sequential": 1},
    )
    httpx_mock.add_response(method="PATCH", url="https://api.tyko-labs.com/runs/r1", json={})
    httpx_mock.add_response(method="PATCH", url="https://api.tyko-labs.com/runs/r1", json={})

    import sys

    original_stdout = sys.stdout
    client = TykoClient()
    with client.start_run(project="p", capture_output=False):
        # sys.stdout must not have been replaced
        assert sys.stdout is original_stdout


# ---------------------------------------------------------------------------
# Deterministic client ID
# ---------------------------------------------------------------------------


def test_client_id_is_deterministic():
    """Same PID + same config always produces the same client UUID."""
    from tyko._client import _make_client_id

    id1 = _make_client_id(api_url="https://api.tyko-labs.com", api_key="key")
    id2 = _make_client_id(api_url="https://api.tyko-labs.com", api_key="key")
    assert id1 == id2


def test_client_id_differs_for_different_keys():
    """Different API keys produce different client IDs."""
    from tyko._client import _make_client_id

    id1 = _make_client_id(api_url="https://api.tyko-labs.com", api_key="key-a")
    id2 = _make_client_id(api_url="https://api.tyko-labs.com", api_key="key-b")
    assert id1 != id2


def test_client_id_differs_for_different_urls():
    """Different API URLs produce different client IDs."""
    from tyko._client import _make_client_id

    id1 = _make_client_id(api_url="https://api.tyko-labs.com", api_key="key")
    id2 = _make_client_id(api_url="https://staging-api.tyko-labs.com", api_key="key")
    assert id1 != id2


# def test_start_run_with_custom_name():
#     """Test starting a run with a custom run name."""
#     client = TykoClient()
#     with client.start_run(project="my-project", name="run-v1") as run:
#         assert run.name == "run-v1"


# def test_run_generates_random_name():
#     """Test that runs generate random names when not provided."""
#     client = TykoClient()
#     with client.start_run(project="my-project") as run:
#         assert run.name is not None
#         assert len(run.name) > 0
