from __future__ import annotations

from datetime import datetime
from io import BytesIO
from urllib.parse import parse_qs

import httpx
import pytest
from astropy.io import fits
from astropy.table import Table
from grad300_client import Grad300Client, conf


def _scan_payload(
    *,
    scan_id: int,
    file_name: str,
    source: str,
    date: str,
) -> dict[str, object]:
    return {
        "id": scan_id,
        "file_path": f"/data/{file_name}",
        "file_name": file_name,
        "size": 1234,
        "ctime": date,
        "mtime": date,
        "object": source,
        "ra": 0.0,
        "dec": 0.0,
        "gain": None,
        "comment": None,
        "date": date,
        "date_obs": date,
        "date_end": date,
        "azimuth": 42.0,
        "elevation": 55.0,
        "nsamples": 10,
    }


def _make_tpi_fits_bytes() -> bytes:
    table_hdu = fits.BinTableHDU.from_columns(
        [
            fits.Column(name="Azimuth", format="E", array=[1.0, 2.0]),
            fits.Column(name="Elevation", format="E", array=[3.0, 4.0]),
        ]
    )
    hdul = fits.HDUList([fits.PrimaryHDU(), table_hdu])
    buffer = BytesIO()
    hdul.writeto(buffer)
    return buffer.getvalue()


def _make_image_fits_bytes() -> bytes:
    hdul = fits.HDUList([fits.PrimaryHDU(data=[[1, 2], [3, 4]])])
    buffer = BytesIO()
    hdul.writeto(buffer)
    return buffer.getvalue()


def test_login_sets_token_and_allows_authenticated_call() -> None:
    def handler(request: httpx.Request) -> httpx.Response:
        if request.url.path == "/api/v1/login/access-token":
            form = parse_qs(request.content.decode())
            assert form["username"] == ["student@example.com"]
            assert form["password"] == ["secret"]
            return httpx.Response(
                200,
                json={"access_token": "token-123", "token_type": "bearer"},
            )

        if request.url.path == "/api/v1/login/test-token":
            assert request.headers["authorization"] == "Bearer token-123"
            return httpx.Response(
                200,
                json={"email": "student@example.com", "is_active": True},
            )

        return httpx.Response(404, json={"detail": "not found"})

    client = Grad300Client(
        "https://grad300.example.org",
        transport=httpx.MockTransport(handler),
    )
    try:
        token = client.login("student@example.com", "secret")
        assert token == "token-123"

        me = client.test_token()
        assert me["email"] == "student@example.com"
    finally:
        client.close()


def test_constructor_auto_login_with_user_password() -> None:
    login_calls = 0

    def handler(request: httpx.Request) -> httpx.Response:
        nonlocal login_calls

        if request.url.path == "/api/v1/login/access-token":
            login_calls += 1
            form = parse_qs(request.content.decode())
            assert form["username"] == ["student@example.com"]
            assert form["password"] == ["secret"]
            return httpx.Response(
                200,
                json={"access_token": "token-from-constructor", "token_type": "bearer"},
            )

        if request.url.path == "/api/v1/login/test-token":
            assert request.headers["authorization"] == "Bearer token-from-constructor"
            return httpx.Response(
                200,
                json={"email": "student@example.com", "is_active": True},
            )

        return httpx.Response(404, json={"detail": "not found"})

    client = Grad300Client(
        "https://grad300.example.org",
        user="student@example.com",
        password="secret",
        transport=httpx.MockTransport(handler),
    )
    try:
        me = client.test_token()
        assert me["email"] == "student@example.com"
        assert login_calls == 1
    finally:
        client.close()


def test_constructor_requires_user_and_password_together() -> None:
    with pytest.raises(
        ValueError, match="Both 'user' and 'password' must be provided together"
    ):
        Grad300Client("https://grad300.example.org", user="student@example.com")


def test_constructor_uses_conf_defaults() -> None:
    def handler(request: httpx.Request) -> httpx.Response:
        if request.url.path == "/api/custom/scans/TPI/":
            # default_page_size from conf should be used when limit/page_size is omitted
            assert request.url.params.get("limit") == "7"
            return httpx.Response(
                200,
                json=[
                    _scan_payload(
                        scan_id=11,
                        file_name="conf-defaults.fits",
                        source="M31",
                        date="2026-03-05T10:00:00+00:00",
                    )
                ],
            )

        return httpx.Response(404, json={"detail": "not found"})

    with (
        conf.set_temp("base_url", "https://grad300.example.org"),
        conf.set_temp("api_prefix", "/api/custom"),
        conf.set_temp("default_page_size", 7),
    ):
        client = Grad300Client(transport=httpx.MockTransport(handler))
        try:
            assert client.base_api_url == "https://grad300.example.org/api/custom"
            table = client.list("TPI")
            assert len(table) == 1
            assert int(table[0]["id"]) == 11
        finally:
            client.close()


def test_query_source_filters_results() -> None:
    def handler(request: httpx.Request) -> httpx.Response:
        if request.url.path == "/api/v1/scans/TPI/":
            # Backend filter is passed when supported by the server.
            assert request.url.params.get("source") == "cass"
            return httpx.Response(
                200,
                json=[
                    _scan_payload(
                        scan_id=1,
                        file_name="20260305-cassiopeia.fits",
                        source="Cassiopeia A",
                        date="2026-03-05T10:00:00+00:00",
                    ),
                    _scan_payload(
                        scan_id=2,
                        file_name="20260305-cygnus.fits",
                        source="Cygnus X-1",
                        date="2026-03-05T11:00:00+00:00",
                    ),
                ],
            )

        return httpx.Response(404, json={"detail": "not found"})

    client = Grad300Client(
        "https://grad300.example.org",
        transport=httpx.MockTransport(handler),
    )
    try:
        scans = client.query_source("cass", scan_type="TPI")
        assert isinstance(scans, Table)
        assert len(scans) == 1
        assert int(scans[0]["id"]) == 1
        assert str(scans[0]["source"]) == "Cassiopeia A"
    finally:
        client.close()


def test_query_date_range_filters_even_without_backend_support() -> None:
    def handler(request: httpx.Request) -> httpx.Response:
        if request.url.path == "/api/v1/scans/Images/":
            # Simulate older backend behavior that ignores date filter params.
            return httpx.Response(
                200,
                json=[
                    _scan_payload(
                        scan_id=5,
                        file_name="old_scan.fits",
                        source="M31",
                        date="2024-01-01T00:00:00+00:00",
                    ),
                    _scan_payload(
                        scan_id=6,
                        file_name="new_scan.fits",
                        source="M31",
                        date="2026-01-01T00:00:00+00:00",
                    ),
                ],
            )

        return httpx.Response(404, json={"detail": "not found"})

    client = Grad300Client(
        "https://grad300.example.org",
        transport=httpx.MockTransport(handler),
    )
    try:
        scans = client.query_date_range(
            scan_type="Images",
            date_from=datetime.fromisoformat("2025-01-01T00:00:00+00:00"),
        )
        assert isinstance(scans, Table)
        assert len(scans) == 1
        assert int(scans[0]["id"]) == 6
        assert str(scans[0]["file_name"]) == "new_scan.fits"
    finally:
        client.close()


def test_download_accepts_table_row(tmp_path) -> None:
    content = b"SIMPLE  =                    T / fake fits payload\n"

    def handler(request: httpx.Request) -> httpx.Response:
        if request.url.path == "/api/v1/scans/TPI/":
            return httpx.Response(
                200,
                json=[
                    _scan_payload(
                        scan_id=42,
                        file_name="row-driven-download.fits",
                        source="Cassiopeia A",
                        date="2026-03-05T10:00:00+00:00",
                    )
                ],
            )

        if request.url.path == "/api/v1/scans/TPI/42/download":
            return httpx.Response(
                200,
                content=content,
            )

        return httpx.Response(404, json={"detail": "not found"})

    client = Grad300Client(
        "https://grad300.example.org",
        transport=httpx.MockTransport(handler),
    )
    try:
        table = client.query_source("cass", scan_type="TPI")
        output_path = client.download(table[0], destination_dir=tmp_path)
        assert not isinstance(output_path, Table)
        assert not isinstance(output_path, fits.HDUList)

        assert output_path.name == "row-driven-download.fits"
        assert output_path.read_bytes() == content
    finally:
        client.close()


def test_download_by_id_returns_table_for_tpi_without_destination_dir() -> None:
    tpi_bytes = _make_tpi_fits_bytes()

    def handler(request: httpx.Request) -> httpx.Response:
        if request.url.path == "/api/v1/scans/TPI/17/download":
            return httpx.Response(
                200,
                content=tpi_bytes,
            )

        return httpx.Response(404, json={"detail": "not found"})

    client = Grad300Client(
        "https://grad300.example.org",
        transport=httpx.MockTransport(handler),
    )
    try:
        table = client.download_by_id(scan_type="TPI", scan_id=17)
        assert isinstance(table, Table)
        assert "Azimuth" in table.colnames
        assert len(table) == 2
    finally:
        client.close()


def test_download_by_id_returns_hdulist_for_non_tpi_without_destination_dir() -> None:
    image_bytes = _make_image_fits_bytes()

    def handler(request: httpx.Request) -> httpx.Response:
        if request.url.path == "/api/v1/scans/Images/9/download":
            return httpx.Response(
                200,
                content=image_bytes,
            )

        return httpx.Response(404, json={"detail": "not found"})

    client = Grad300Client(
        "https://grad300.example.org",
        transport=httpx.MockTransport(handler),
    )
    try:
        hdul = client.download_by_id(scan_type="Images", scan_id=9)
        assert isinstance(hdul, fits.HDUList)
        assert hdul[0].data is not None
        assert hdul[0].data.shape == (2, 2)
        hdul.close()
    finally:
        client.close()


def test_download_by_id_uses_filename_from_headers(tmp_path) -> None:
    content = b"SIMPLE  =                    T / fake fits payload\n"

    def handler(request: httpx.Request) -> httpx.Response:
        if request.url.path == "/api/v1/scans/TPI/7/download":
            return httpx.Response(
                200,
                content=content,
                headers={"content-disposition": 'attachment; filename="scan-7.fits"'},
            )

        return httpx.Response(404, json={"detail": "not found"})

    client = Grad300Client(
        "https://grad300.example.org",
        transport=httpx.MockTransport(handler),
    )
    try:
        output_path = client.download_by_id(
            scan_type="TPI",
            scan_id=7,
            destination_dir=tmp_path,
        )
        assert not isinstance(output_path, Table)
        assert not isinstance(output_path, fits.HDUList)

        assert output_path.name == "scan-7.fits"
        assert output_path.read_bytes() == content
    finally:
        client.close()
