import os
import platform
import threading
from pathlib import Path
from tempfile import gettempdir
from unittest import mock
from unittest.mock import MagicMock, patch

import aiohttp
import pytest
from aiohttp import ClientConnectorError, ClientTimeout
from pytest_socket import SocketConnectBlockedError

import parfive
from parfive.config import SessionConfig
from parfive.downloader import Downloader, FailedDownload, Results, Token
from parfive.utils import sha256sum

skip_windows = pytest.mark.skipif(platform.system() == "Windows", reason="Windows.")


def validate_test_file(f):
    assert len(f) == 1
    assert Path(f[0]).name == "testfile.fits"
    assert sha256sum(f[0]) == "a1c58cd340e3bd33f94524076f1fa5cf9a7f13c59d5272a9d4bc0b5bc436d9b3"


def test_setup():
    dl = Downloader()

    assert isinstance(dl, Downloader)

    assert len(dl.http_queue) == 0
    assert len(dl.ftp_queue) == 0
    assert dl._generate_tokens().qsize() == 5


def test_download(httpserver, tmpdir):
    tmpdir = str(tmpdir)
    httpserver.serve_content(
        "SIMPLE  = T", headers={"Content-Disposition": "attachment; filename=testfile.fits"}
    )
    dl = Downloader()

    dl.enqueue_file(httpserver.url, path=Path(tmpdir), max_splits=None)

    assert dl.queued_downloads == 1

    f = dl.download()
    validate_test_file(f)


def test_simple_download(httpserver, tmpdir):
    tmpdir = str(tmpdir)
    httpserver.serve_content(
        "SIMPLE  = T", headers={"Content-Disposition": "attachment; filename=testfile.fits"}
    )

    f = Downloader.simple_download([httpserver.url], path=Path(tmpdir))
    validate_test_file(f)


def test_changed_max_conn(httpserver, tmpdir):
    # Check that changing max_conn works after creating Downloader
    tmpdir = str(tmpdir)
    httpserver.serve_content(
        "SIMPLE  = T", headers={"Content-Disposition": "attachment; filename=testfile.fits"}
    )
    dl = Downloader(max_conn=4)
    dl.enqueue_file(httpserver.url, path=Path(tmpdir), max_splits=None)
    dl.max_conn = 3

    f = dl.download()
    validate_test_file(f)


@pytest.mark.asyncio
@pytest.mark.parametrize("use_aiofiles", [True, False])
async def test_async_download(httpserver, tmpdir, use_aiofiles):
    httpserver.serve_content(
        "SIMPLE  = T", headers={"Content-Disposition": "attachment; filename=testfile.fits"}
    )
    dl = Downloader(config=SessionConfig(use_aiofiles=use_aiofiles))

    dl.enqueue_file(httpserver.url, path=Path(tmpdir), max_splits=None)

    assert dl.queued_downloads == 1

    f = await dl.run_download()
    validate_test_file(f)


def test_download_ranged_http(httpserver, tmpdir):
    tmpdir = str(tmpdir)
    httpserver.serve_content(
        "SIMPLE  = T", headers={"Content-Disposition": "attachment; filename=testfile.fits"}
    )
    dl = Downloader()

    dl.enqueue_file(httpserver.url, path=Path(tmpdir))

    assert dl.queued_downloads == 1

    f = dl.download()
    validate_test_file(f)


def test_regression_download_ranged_http(httpserver, tmpdir):
    tmpdir = str(tmpdir)
    httpserver.serve_content(
        "S",
        headers={
            "Content-Disposition": "attachment; filename=testfile.fits",
            "Accept-Ranges": "bytes",
        },
    )
    dl = Downloader()

    dl.enqueue_file(httpserver.url, path=Path(tmpdir))

    assert dl.queued_downloads == 1

    f = dl.download()
    assert len(f.errors) == 0, f.errors


def test_download_partial(httpserver, tmpdir):
    tmpdir = str(tmpdir)
    httpserver.serve_content("SIMPLE  = T")
    dl = Downloader()

    dl.enqueue_file(httpserver.url, filename=lambda resp, url: Path(tmpdir) / "filename")
    f = dl.download()
    assert len(f) == 1

    # strip the http://
    assert "filename" in f[0]


def test_empty_download(tmpdir):
    dl = Downloader()

    f = dl.download()
    assert len(f) == 0


def test_download_filename(httpserver, tmpdir):
    httpserver.serve_content("SIMPLE  = T")

    fname = "testing123"
    filename = str(tmpdir.join(fname))
    with open(filename, "w") as fh:
        fh.write("SIMPLE = T")

    dl = Downloader()

    dl.enqueue_file(httpserver.url, filename=filename, chunksize=200)
    f = dl.download()

    assert isinstance(f, Results)
    assert len(f) == 1

    assert f[0] == filename


def test_download_no_overwrite(httpserver, tmpdir):
    httpserver.serve_content("SIMPLE  = T")

    fname = "testing123"
    filename = str(tmpdir.join(fname))
    with open(filename, "w") as fh:
        fh.write("Hello world")

    dl = Downloader()

    dl.enqueue_file(httpserver.url, filename=filename, chunksize=200)
    f = dl.download()

    assert isinstance(f, Results)
    assert len(f) == 1

    assert f[0] == filename

    with open(filename) as fh:
        # If the contents is the same as when we wrote it, it hasn't been
        # overwritten
        assert fh.read() == "Hello world"


def test_download_overwrite(httpserver, tmpdir):
    httpserver.serve_content("SIMPLE  = T")

    fname = "testing123"
    filename = str(tmpdir.join(fname))
    with open(filename, "w") as fh:
        fh.write("Hello world")

    dl = Downloader(overwrite=True)

    dl.enqueue_file(httpserver.url, filename=filename, chunksize=200)
    f = dl.download()

    assert isinstance(f, Results)
    assert len(f) == 1

    assert f[0] == filename

    with open(filename) as fh:
        assert fh.read() == "SIMPLE  = T"


def test_download_unique(httpserver, tmpdir):
    httpserver.serve_content("SIMPLE  = T")

    fname = "testing123"
    filename = str(tmpdir.join(fname))

    filenames = [filename, filename + ".fits", filename + ".fits.gz"]

    dl = Downloader(overwrite="unique")

    # Write files to both the target filenames.
    for fn in filenames:
        with open(fn, "w") as fh:
            fh.write("Hello world")

            dl.enqueue_file(httpserver.url, filename=fn, chunksize=200)

    f = dl.download()

    assert isinstance(f, Results)
    assert len(f) == len(filenames)

    for fn in f:
        assert fn not in filenames
        assert f"{fname}.1" in fn


def test_retrieve_some_content(testserver, tmpdir):
    """
    Test that the downloader handles errors properly.
    """
    tmpdir = str(tmpdir)
    dl = Downloader()

    nn = 5
    for i in range(nn):
        dl.enqueue_file(testserver.url, path=tmpdir)

    f = dl.download()

    assert len(f) == nn - 1
    assert len(f.errors) == 1


def test_no_progress(httpserver, tmpdir, capsys):
    tmpdir = str(tmpdir)
    httpserver.serve_content("SIMPLE  = T")
    dl = Downloader(progress=False)

    dl.enqueue_file(httpserver.url, path=tmpdir)

    dl.download()

    # Check that there was not stdout
    captured = capsys.readouterr().out
    assert not captured


def throwerror(*args, **kwargs):
    raise ValueError("Out of Cheese.")


@patch("parfive.downloader.default_name", throwerror)
def test_raises_other_exception(httpserver, tmpdir):
    tmpdir = str(tmpdir)
    httpserver.serve_content("SIMPLE  = T")
    dl = Downloader()

    dl.enqueue_file(httpserver.url, path=tmpdir)
    res = dl.download()
    assert isinstance(res.errors[0].exception, ValueError)


def test_token():
    t = Token(5)

    assert "5" in repr(t)
    assert "5" in str(t)


def test_failed_download():
    err = FailedDownload("wibble", "bbc.co.uk", "running away")

    assert "bbc.co.uk" in repr(err)
    assert "bbc.co.uk" in repr(err)

    assert "running away" in str(err)
    assert "running away" in str(err)


def test_results():
    res = Results()

    res.append("hello")

    res.add_error("wibble", "notaurl", "out of cheese")

    assert "notaurl" in repr(res)
    assert "hello" in repr(res)
    assert "out of cheese" in repr(res)


def test_notaurl(tmpdir):
    tmpdir = str(tmpdir)

    dl = Downloader(progress=False)

    dl.enqueue_file("http://notaurl.wibble/file", path=tmpdir)

    f = dl.download()

    assert len(f.errors) == 1
    assert isinstance(f.errors[0].exception, aiohttp.ClientConnectionError)


def test_wrongscheme(tmpdir):
    tmpdir = str(tmpdir)

    dl = Downloader(progress=False)

    with pytest.raises(ValueError, match="URL must start with either"):
        dl.enqueue_file("webcal://notaurl.wibble/file", path=tmpdir)


def test_retry(tmpdir, testserver):
    tmpdir = str(tmpdir)
    dl = Downloader()

    nn = 5
    for i in range(nn):
        dl.enqueue_file(testserver.url, path=tmpdir)

    f = dl.download()

    assert len(f) == nn - 1
    assert len(f.errors) == 1

    f2 = dl.retry(f)

    assert len(f2) == nn
    assert len(f2.errors) == 0


def test_empty_retry():
    f = Results()
    dl = Downloader()

    dl.retry(f)


def test_done_callback_error(tmpdir, testserver):
    tmpdir = str(tmpdir)

    def done_callback(filepath, url, error):
        if error is not None:
            (Path(gettempdir()) / "callback.error").touch()

    dl = Downloader(config=SessionConfig(done_callbacks=[done_callback]))

    nn = 5
    for i in range(nn):
        dl.enqueue_file(testserver.url, path=tmpdir)

    f = dl.download()

    assert (Path(gettempdir()) / "callback.error").exists()
    (Path(gettempdir()) / "callback.error").unlink()


@skip_windows
@pytest.mark.allow_hosts(True)
def test_ftp(tmpdir):
    tmpdir = str(tmpdir)
    dl = Downloader()

    dl.enqueue_file("ftp://ftp.swpc.noaa.gov/pub/warehouse/2011/2011_SRS.tar.gz", path=tmpdir)
    dl.enqueue_file("ftp://ftp.swpc.noaa.gov/pub/warehouse/2011/2013_SRS.tar.gz", path=tmpdir)
    dl.enqueue_file("ftp://ftp.swpc.noaa.gov/pub/_SRS.tar.gz", path=tmpdir)
    dl.enqueue_file("ftp://notaserver/notafile.fileL", path=tmpdir)

    f = dl.download()
    assert len(f) == 1
    assert len(f.errors) == 3


@skip_windows
@pytest.mark.allow_hosts(True)
def test_ftp_pasv_command(tmpdir):
    tmpdir = str(tmpdir)
    dl = Downloader()
    dl.enqueue_file(
        "ftp://ftp.ngdc.noaa.gov/STP/swpc_products/daily_reports/solar_region_summaries/2002/04/20020414SRS.txt",
        path=tmpdir,
        passive_commands=["pasv"],
    )
    assert dl.queued_downloads == 1
    f = dl.download()
    assert len(f) == 1
    assert len(f.errors) == 0


@skip_windows
@pytest.mark.allow_hosts(True)
def test_ftp_http(tmpdir, httpserver):
    tmpdir = str(tmpdir)
    httpserver.serve_content("SIMPLE  = T")
    dl = Downloader()

    dl.enqueue_file("ftp://ftp.swpc.noaa.gov/pub/warehouse/2011/2011_SRS.tar.gz", path=tmpdir)
    dl.enqueue_file("ftp://ftp.swpc.noaa.gov/pub/warehouse/2011/2013_SRS.tar.gz", path=tmpdir)
    dl.enqueue_file("ftp://ftp.swpc.noaa.gov/pub/_SRS.tar.gz", path=tmpdir)
    dl.enqueue_file("ftp://notaserver/notafile.fileL", path=tmpdir)
    dl.enqueue_file(httpserver.url, path=tmpdir)
    dl.enqueue_file("http://noaurl.notadomain/noafile", path=tmpdir)

    assert dl.queued_downloads == 6

    f = dl.download()
    assert len(f) == 2
    assert len(f.errors) == 4


def test_default_user_agent(httpserver, tmpdir):
    tmpdir = str(tmpdir)
    httpserver.serve_content(
        "SIMPLE  = T", headers={"Content-Disposition": "attachment; filename=testfile.fits"}
    )

    dl = Downloader()
    dl.enqueue_file(httpserver.url, path=Path(tmpdir), max_splits=None)

    assert dl.queued_downloads == 1

    dl.download()

    assert "User-Agent" in httpserver.requests[0].headers
    assert (
        httpserver.requests[0].headers["User-Agent"]
        == f"parfive/{parfive.__version__} aiohttp/{aiohttp.__version__} python/{platform.python_version()}"
    )


def test_custom_user_agent(httpserver, tmpdir):
    tmpdir = str(tmpdir)
    httpserver.serve_content(
        "SIMPLE  = T", headers={"Content-Disposition": "attachment; filename=testfile.fits"}
    )

    dl = Downloader(config=SessionConfig(headers={"User-Agent": "test value 299792458"}))
    dl.enqueue_file(httpserver.url, path=Path(tmpdir), max_splits=None)

    assert dl.queued_downloads == 1

    dl.download()

    assert "User-Agent" in httpserver.requests[0].headers
    assert httpserver.requests[0].headers["User-Agent"] == "test value 299792458"


@patch.dict(os.environ, {"HTTP_PROXY": "http_proxy_url", "HTTPS_PROXY": "https_proxy_url"})
@pytest.mark.parametrize(
    "url,proxy",
    [
        ("http://test.example.com", "http_proxy_url"),
        ("https://test.example.com", "https_proxy_url"),
    ],
)
def test_proxy_passed_as_kwargs_to_get(tmpdir, url, proxy):
    with mock.patch("aiohttp.client.ClientSession._request", new_callable=mock.MagicMock) as patched:
        dl = Downloader()
        dl.enqueue_file(url, path=Path(tmpdir), max_splits=None)

        assert dl.queued_downloads == 1

        dl.download()

    assert patched.called, "`ClientSession._request` not called"
    assert list(patched.call_args) == [
        ("GET", url),
        {
            "allow_redirects": True,
            "timeout": ClientTimeout(total=0, connect=None, sock_read=90, sock_connect=None),
            "proxy": proxy,
        },
    ]


def test_http_callback_success(httpserver, tmpdir):
    # Test callback on successful download
    httpserver.serve_content(
        "SIMPLE  = T", headers={"Content-Disposition": "attachment; filename=testfile.fits"}
    )

    cb = MagicMock()
    dl = Downloader(config=SessionConfig(done_callbacks=[cb]))
    dl.enqueue_file(httpserver.url, path=tmpdir, max_splits=None)

    assert dl.queued_downloads == 1

    dl.download()

    assert cb.call_count == 1
    cb_path, cb_url, cb_status = cb.call_args[0]
    assert cb_path == tmpdir / "testfile.fits"
    assert httpserver.url == cb_url
    assert cb_status is None


def test_http_callback_fail(httpserver, tmpdir):
    # Test callback on failed download
    cb = MagicMock()
    dl = Downloader(config=SessionConfig(done_callbacks=[cb]))
    url = "http://test.com/myfile.txt"
    dl.enqueue_file(url, path=tmpdir, max_splits=None)

    assert dl.queued_downloads == 1

    dl.download()

    assert cb.call_count == 1
    cb_path, cb_url, cb_status = cb.call_args[0]
    assert cb_path is None
    assert url == cb_url
    assert isinstance(cb_status, (SocketConnectBlockedError, ClientConnectorError))


@pytest.mark.allow_hosts(True)
def test_ftp_callback_success(tmpdir):
    cb = MagicMock()
    dl = Downloader(config=SessionConfig(done_callbacks=[cb]))
    url = "ftp://ftp.swpc.noaa.gov/pub/warehouse/2011/2011_SRS.tar.gz"
    dl.enqueue_file(url, path=str(tmpdir))

    assert dl.queued_downloads == 1

    dl.download()

    assert cb.call_count == 1
    cb_path, cb_url, cb_status = cb.call_args[0]
    assert cb_path == tmpdir / "2011_SRS.tar.gz"
    assert url == cb_url
    assert cb_status is None


@mock.patch("aioftp.Client.context", side_effect=ConnectionRefusedError())
def test_ftp_callback_error(tmpdir):
    # Download should fail as not marked with allowed hosts
    cb = MagicMock()
    dl = Downloader(config=SessionConfig(done_callbacks=[cb]))
    url = "ftp://127.0.0.1/nosuchfile.txt"
    dl.enqueue_file(url, path=str(tmpdir))

    assert dl.queued_downloads == 1

    dl.download()

    assert cb.call_count == 1
    cb_path, cb_url, cb_status = cb.call_args[0]
    assert cb_path is None
    assert cb_url == url
    assert isinstance(cb_status, ConnectionRefusedError)


class CustomThread(threading.Thread):
    def __init__(self, *args, **kwargs):
        self.result = None
        super().__init__(*args, **kwargs)

    def run(self):
        try:
            self.result = self._target(*self._args, **self._kwargs)
        finally:
            del self._target, self._args, self._kwargs


@skip_windows
def test_download_out_of_main_thread(httpserver, tmpdir, recwarn):
    tmpdir = str(tmpdir)
    httpserver.serve_content(
        "SIMPLE  = T", headers={"Content-Disposition": "attachment; filename=testfile.fits"}
    )
    dl = Downloader()

    dl.enqueue_file(httpserver.url, path=Path(tmpdir), max_splits=None)

    thread = CustomThread(target=dl.download)
    thread.start()
    thread.join()

    validate_test_file(thread.result)

    # We use recwarn here as for some reason pytest.warns did not reliably pickup this warning.
    assert len(recwarn) > 0
    assert any(
        [
            "This download has been started in a thread which is not the main thread. You will not be able to interrupt the download."
            == w.message.args[0]
            for w in recwarn
        ]
    )
