"""Tests for SkytellsClient, Prediction, PredictionsAPI, and ModelsAPI."""

import json
import unittest.mock as mock
from typing import Any, Dict
from unittest.mock import MagicMock, patch

import pytest

import skytells
from skytells import (
    Skytells,
    SkytellsClient,
    SkytellsError,
    Prediction,
    PredictionsAPI,
    ModelsAPI,
    API_BASE_URL,
)
from skytells._client import TERMINAL_STATUSES
from skytells._http import HTTP
from skytells.types.predict import PredictionStatus


# ── Fixtures ──────────────────────────────────────────────────────────────────

def make_prediction_response(
    id="pred_abc",
    status="succeeded",
    output=["https://example.com/image.png"],
    **kwargs,
) -> Dict[str, Any]:
    return {
        "id": id,
        "status": status,
        "output": output,
        "input": {"prompt": "test"},
        "type": "inference",
        "stream": False,
        "created_at": "2026-01-01T00:00:00Z",
        "started_at": "2026-01-01T00:00:01Z",
        "completed_at": "2026-01-01T00:00:05Z",
        "updated_at": "2026-01-01T00:00:05Z",
        "privacy": "public",
        **kwargs,
    }


def make_http_mock(return_value: Any = None) -> MagicMock:
    http = MagicMock(spec=HTTP)
    http.request.return_value = return_value or make_prediction_response()
    return http


# ── Factory / exports ─────────────────────────────────────────────────────────

class TestFactory:
    def test_skytells_returns_client(self):
        client = Skytells("sk-test")
        assert isinstance(client, SkytellsClient)

    def test_default_base_url_is_set(self):
        client = SkytellsClient("sk-test")
        assert client._http._base_url == API_BASE_URL

    def test_custom_base_url_is_respected(self):
        client = SkytellsClient("sk-test", base_url="https://custom.example.com")
        assert client._http._base_url == "https://custom.example.com"

    def test_create_client_alias(self):
        assert skytells.create_client is Skytells

    def test_default_exports(self):
        assert hasattr(skytells, "SkytellsClient")
        assert hasattr(skytells, "AsyncSkytellsClient")
        assert hasattr(skytells, "Prediction")
        assert hasattr(skytells, "PredictionsAPI")
        assert hasattr(skytells, "ModelsAPI")
        assert hasattr(skytells, "SkytellsError")
        assert hasattr(skytells, "API_BASE_URL")
        assert hasattr(skytells, "__version__")

    def test_version(self):
        assert skytells.__version__ == "1.0.0"

    def test_api_base_url_exported(self):
        assert API_BASE_URL == "https://api.skytells.ai/v1"

    def test_client_has_predictions_api(self):
        client = Skytells("sk-test")
        assert isinstance(client.predictions, PredictionsAPI)

    def test_client_has_models_api(self):
        client = Skytells("sk-test")
        assert isinstance(client.models, ModelsAPI)


# ── Prediction object ─────────────────────────────────────────────────────────

class TestPrediction:
    def setup_method(self):
        self.http = make_http_mock()

    def test_id(self):
        data = make_prediction_response(id="pred_xyz")
        p = Prediction(self.http, data)
        assert p.id == "pred_xyz"

    def test_status(self):
        data = make_prediction_response(status="succeeded")
        p = Prediction(self.http, data)
        assert p.status == "succeeded"

    def test_output_string(self):
        data = make_prediction_response(output="https://example.com/img.png")
        p = Prediction(self.http, data)
        assert p.output == "https://example.com/img.png"

    def test_output_list(self):
        data = make_prediction_response(output=["https://a.com", "https://b.com"])
        p = Prediction(self.http, data)
        assert isinstance(p.output, list)
        assert len(p.output) == 2

    def test_output_none(self):
        data = make_prediction_response(status="pending")
        data.pop("output", None)
        p = Prediction(self.http, data)
        assert p.output is None

    def test_response_returns_full_dict(self):
        data = make_prediction_response()
        p = Prediction(self.http, data)
        assert p.response is data

    def test_raw_returns_full_dict(self):
        data = make_prediction_response()
        p = Prediction(self.http, data)
        assert p.raw() is data

    # outputs()

    def test_outputs_none(self):
        data = make_prediction_response(status="pending")
        data.pop("output", None)
        p = Prediction(self.http, data)
        assert p.outputs() is None

    def test_outputs_empty_list_returns_empty_list(self):
        # JS: [] is truthy, passes the falsy-check, so JS returns []
        # Python must match: output=[] → outputs() returns []
        data = make_prediction_response(output=[])
        p = Prediction(self.http, data)
        result = p.outputs()
        assert result == []

    def test_outputs_string(self):
        data = make_prediction_response(output="https://img.png")
        p = Prediction(self.http, data)
        assert p.outputs() == "https://img.png"

    def test_outputs_single_element_list_unwrapped(self):
        data = make_prediction_response(output=["https://img.png"])
        p = Prediction(self.http, data)
        result = p.outputs()
        assert result == "https://img.png"
        assert isinstance(result, str)

    def test_outputs_multi_element_list_kept(self):
        data = make_prediction_response(output=["https://a.png", "https://b.png"])
        p = Prediction(self.http, data)
        result = p.outputs()
        assert isinstance(result, list)
        assert len(result) == 2

    # cancel / delete

    def test_cancel_calls_http(self):
        cancel_response = make_prediction_response(status="cancelled")
        self.http.request.return_value = cancel_response
        data = make_prediction_response(id="pred_abc")
        p = Prediction(self.http, data)
        result = p.cancel()
        self.http.request.assert_called_once_with("POST", "/predictions/pred_abc/cancel")
        assert result["status"] == "cancelled"

    def test_delete_calls_http(self):
        self.http.request.return_value = make_prediction_response()
        data = make_prediction_response(id="pred_del")
        p = Prediction(self.http, data)
        p.delete()
        self.http.request.assert_called_once_with("DELETE", "/predictions/pred_del/delete")

    def test_repr(self):
        p = Prediction(self.http, make_prediction_response(id="pred_r", status="succeeded"))
        assert "pred_r" in repr(p)
        assert "succeeded" in repr(p)


# ── PredictionsAPI ─────────────────────────────────────────────────────────────

class TestPredictionsAPI:
    def setup_method(self):
        self.http = make_http_mock()
        self.api = PredictionsAPI(self.http)

    def test_create_sets_await_false(self):
        self.api.create({"model": "flux-pro", "input": {"prompt": "cat"}})
        call_body = self.http.request.call_args[0][2]
        assert call_body["await"] is False

    def test_create_posts_to_predict(self):
        self.api.create({"model": "flux-pro", "input": {}})
        self.http.request.assert_called_once()
        assert self.http.request.call_args[0][0] == "POST"
        assert self.http.request.call_args[0][1] == "/predict"

    def test_get_calls_prediction_by_id(self):
        self.api.get("pred_abc")
        self.http.request.assert_called_once_with("GET", "/predictions/pred_abc")

    def test_list_no_params(self):
        self.http.request.return_value = {
            "data": [],
            "pagination": {"current_page": 1, "per_page": 10, "total": 0, "last_page": 1},
        }
        result = self.api.list()
        self.http.request.assert_called_once_with("GET", "/predictions")
        assert result.data == []

    def test_list_with_params(self):
        self.http.request.return_value = {
            "data": [],
            "pagination": {"current_page": 2, "per_page": 10, "total": 0, "last_page": 1},
        }
        self.api.list(page=2, since="2026-01-01", until="2026-03-16", model="flux-pro")
        called_path = self.http.request.call_args[0][1]
        assert "page=2" in called_path
        assert "from=2026-01-01" in called_path
        assert "to=2026-03-16" in called_path
        assert "model=flux-pro" in called_path


# ── ModelsAPI ─────────────────────────────────────────────────────────────────

class TestModelsAPI:
    def setup_method(self):
        self.http = make_http_mock(return_value=[])
        self.api = ModelsAPI(self.http)

    def test_list_calls_models_endpoint(self):
        self.api.list()
        self.http.request.assert_called_once_with("GET", "/models")

    def test_list_with_fields(self):
        self.api.list(fields=["input_schema"])
        called_path = self.http.request.call_args[0][1]
        assert "fields=input_schema" in called_path

    def test_list_with_multiple_fields(self):
        self.api.list(fields=["input_schema", "output_schema"])
        called_path = self.http.request.call_args[0][1]
        assert "input_schema" in called_path
        assert "output_schema" in called_path

    def test_get_calls_model_by_slug(self):
        self.http.request.return_value = {"name": "Flux Pro", "type": "image"}
        self.api.get("flux-pro")
        self.http.request.assert_called_once_with("GET", "/model/flux-pro")

    def test_get_with_fields(self):
        self.http.request.return_value = {}
        self.api.get("flux-pro", fields=["input_schema"])
        called_path = self.http.request.call_args[0][1]
        assert "fields=input_schema" in called_path


# ── SkytellsClient ─────────────────────────────────────────────────────────────

class TestSkytellsClientRun:
    def _make_client_with_mock_http(self, http: MagicMock) -> SkytellsClient:
        client = SkytellsClient("sk-test")
        client._http = http
        client.predictions = PredictionsAPI(http)
        client.models = ModelsAPI(http)
        return client

    def test_run_returns_prediction(self):
        http = make_http_mock(make_prediction_response(status="succeeded"))
        client = self._make_client_with_mock_http(http)
        result = client.run("flux-pro", input={"prompt": "test"})
        assert isinstance(result, Prediction)
        assert result.status == "succeeded"

    def test_run_sets_await_true(self):
        http = make_http_mock(make_prediction_response())
        client = self._make_client_with_mock_http(http)
        client.run("flux-pro", input={"prompt": "test"})
        body = http.request.call_args[0][2]
        assert body.get("await") is True

    def test_run_failed_raises_error(self):
        http = make_http_mock(make_prediction_response(status="failed", output=None))
        client = self._make_client_with_mock_http(http)
        with pytest.raises(SkytellsError) as exc_info:
            client.run("flux-pro", input={"prompt": "test"})
        assert exc_info.value.error_id == "PREDICTION_FAILED"

    def test_run_with_on_progress_uses_polling(self):
        pending = make_prediction_response(id="pred_p", status="pending", output=None)
        succeeded = make_prediction_response(id="pred_p", status="succeeded")

        http = MagicMock(spec=HTTP)
        # create() → pending, get() → succeeded
        http.request.side_effect = [pending, succeeded]

        client = self._make_client_with_mock_http(http)

        progress_calls = []
        with mock.patch("time.sleep"):
            result = client.run(
                "flux-pro",
                input={"prompt": "test"},
                on_progress=lambda p: progress_calls.append(p["status"]),
            )

        assert isinstance(result, Prediction)
        assert result.status == "succeeded"
        assert "succeeded" in progress_calls

    def test_predict_posts_payload(self):
        http = make_http_mock(make_prediction_response())
        client = self._make_client_with_mock_http(http)
        client.predict({"model": "flux-pro", "input": {}, "await": False})
        assert http.request.call_args[0][0] == "POST"
        assert http.request.call_args[0][1] == "/predict"


class TestSkytellsClientWait:
    def _make_client(self, responses):
        http = MagicMock(spec=HTTP)
        http.request.side_effect = responses
        client = SkytellsClient("sk-test")
        client._http = http
        client.predictions = PredictionsAPI(http)
        return client

    def test_wait_returns_immediately_on_terminal(self):
        prediction = make_prediction_response(status="succeeded")
        client = SkytellsClient("sk-test")
        result = client.wait(prediction)
        assert result["status"] == "succeeded"

    def test_wait_polls_until_terminal(self):
        http = MagicMock(spec=HTTP)
        http.request.return_value = make_prediction_response(status="succeeded")

        client = SkytellsClient("sk-test")
        client._http = http
        client.predictions = PredictionsAPI(http)

        pending = make_prediction_response(id="pred_w", status="pending", output=None)
        with mock.patch("time.sleep"):
            result = client.wait(pending)
        assert result["status"] == "succeeded"

    def test_wait_timeout_raises(self):
        http = MagicMock(spec=HTTP)
        http.request.return_value = make_prediction_response(status="processing", output=None)

        client = SkytellsClient("sk-test")
        client._http = http
        client.predictions = PredictionsAPI(http)

        pending = make_prediction_response(id="pred_t", status="pending", output=None)
        with mock.patch("time.sleep"), mock.patch("time.monotonic", side_effect=[0, 0, 200]):
            with pytest.raises(SkytellsError) as exc_info:
                client.wait(pending, options={"max_wait": 100})
        assert exc_info.value.error_id == "WAIT_TIMEOUT"

    def test_wait_calls_progress(self):
        http = MagicMock(spec=HTTP)
        http.request.return_value = make_prediction_response(status="succeeded")

        client = SkytellsClient("sk-test")
        client._http = http
        client.predictions = PredictionsAPI(http)

        pending = make_prediction_response(id="pred_prog", status="pending", output=None)
        calls = []
        with mock.patch("time.sleep"):
            client.wait(pending, on_progress=lambda p: calls.append(p["status"]))
        assert len(calls) >= 1


class TestQueueDispatch:
    def setup_method(self):
        http = make_http_mock(make_prediction_response(status="pending", output=None))
        self.client = SkytellsClient("sk-test")
        self.client._http = http
        self.client.predictions = PredictionsAPI(http)
        self.http = http

    def test_queue_adds_items(self):
        self.client.queue({"model": "flux-pro", "input": {"prompt": "cat"}})
        self.client.queue({"model": "flux-pro", "input": {"prompt": "dog"}})
        assert len(self.client._queue) == 2

    def test_dispatch_clears_queue(self):
        self.client.queue({"model": "flux-pro", "input": {"prompt": "cat"}})
        self.client.dispatch()
        assert len(self.client._queue) == 0

    def test_dispatch_returns_results(self):
        self.client.queue({"model": "flux-pro", "input": {"prompt": "cat"}})
        self.client.queue({"model": "flux-pro", "input": {"prompt": "dog"}})
        results = self.client.dispatch()
        assert len(results) == 2

    def test_dispatch_empty_queue(self):
        results = self.client.dispatch()
        assert results == []


class TestLifecycleMethods:
    def setup_method(self):
        self.http = make_http_mock()
        self.client = SkytellsClient("sk-test")
        self.client._http = self.http

    def test_stream_prediction(self):
        self.client.stream_prediction("pred_abc")
        self.http.request.assert_called_with("GET", "/predictions/pred_abc/stream")

    def test_cancel_prediction(self):
        self.client.cancel_prediction("pred_abc")
        self.http.request.assert_called_with("POST", "/predictions/pred_abc/cancel")

    def test_delete_prediction(self):
        self.client.delete_prediction("pred_abc")
        self.http.request.assert_called_with("DELETE", "/predictions/pred_abc/delete")


class TestDeprecatedMethods:
    def setup_method(self):
        self.http = MagicMock(spec=HTTP)
        self.client = SkytellsClient("sk-test")
        self.client._http = self.http
        self.client.predictions = PredictionsAPI(self.http)
        self.client.models = ModelsAPI(self.http)

    def test_list_predictions_deprecated(self):
        self.http.request.return_value = {
            "data": [],
            "pagination": {"current_page": 1, "per_page": 10, "total": 0, "last_page": 1},
        }
        with pytest.warns(DeprecationWarning):
            self.client.list_predictions()

    def test_list_models_deprecated(self):
        self.http.request.return_value = []
        with pytest.warns(DeprecationWarning):
            self.client.list_models()

    def test_get_prediction_deprecated(self):
        self.http.request.return_value = make_prediction_response()
        with pytest.warns(DeprecationWarning):
            self.client.get_prediction("pred_abc")

    def test_get_model_deprecated(self):
        self.http.request.return_value = {}
        with pytest.warns(DeprecationWarning):
            self.client.get_model("flux-pro")
