import os
import tempfile
from unittest.mock import MagicMock, patch

import httpx
import pytest

from swifttools.swift_too.base.schemas import BaseSchema
from swifttools.swift_too.swift.data import SwiftData, SwiftDataFile, TOOAPIDownloadData


class TestSwiftData:
    def test_init(self):
        """Test SwiftData initialization"""
        data = SwiftData(obs_id="00012345001", autosubmit=False)
        assert data.obs_id == "00012345001"

    def test_all_property(self, swift_data_basic):
        """Test the 'all' property"""
        data = swift_data_basic
        assert data.all is False
        data.xrt = True
        data.uvot = True
        data.bat = True
        data.log = True
        data.auxil = True
        data.tdrss = True
        assert data.all is True

    def test_all_setter_xrt(self, swift_data_basic):
        """Test the 'all' property setter - xrt"""
        data = swift_data_basic
        data.all = True
        assert data.xrt is True

    def test_all_setter_uvot(self, swift_data_basic):
        """Test the 'all' property setter - uvot"""
        data = swift_data_basic
        data.all = True
        assert data.uvot is True

    def test_all_setter_bat(self, swift_data_basic):
        """Test the 'all' property setter - bat"""
        data = swift_data_basic
        data.all = True
        assert data.bat is True

    def test_all_setter_log(self, swift_data_basic):
        """Test the 'all' property setter - log"""
        data = swift_data_basic
        data.all = True
        assert data.log is True

    def test_all_setter_auxil(self, swift_data_basic):
        """Test the 'all' property setter - auxil"""
        data = swift_data_basic
        data.all = True
        assert data.auxil is True

    def test_all_setter_tdrss(self, swift_data_basic):
        """Test the 'all' property setter - tdrss"""
        data = swift_data_basic
        data.all = True
        assert data.tdrss is True

    def test_getitem_first_filename(self, swift_data_basic, swift_data_file_basic):
        """Test indexing - first filename"""
        data = swift_data_basic
        # Use SwiftDataFile instances for entries
        file1 = swift_data_file_basic
        file2 = SwiftDataFile(filename="file2", path="/", url="http://example.com/file2", type="file")
        data.entries = [file1, file2]
        assert data[0].filename == "test.fits"

    def test_getitem_second_filename(self, swift_data_basic, swift_data_file_basic):
        """Test indexing - second filename"""
        data = swift_data_basic
        # Use SwiftDataFile instances for entries
        file1 = swift_data_file_basic
        file2 = SwiftDataFile(filename="file2", path="/", url="http://example.com/file2", type="file")
        data.entries = [file1, file2]
        assert data[1].filename == "file2"

    def test_table_property(self, swift_data_basic):
        """Test _table property"""
        data = swift_data_basic
        data.entries = []
        _, table = data._table
        assert table == []

    @pytest.mark.usefixtures("mock_boto3")
    def test_post_process_aws(self):
        """Test _post_process with AWS setup"""
        data = SwiftData(obs_id="00012345001", uksdc=False, itsdc=False, aws=True, autosubmit=False)
        data._post_process()
        # Should have set up S3 client
        assert data._s3 is not None

    def test_download_no_files(self, swift_data_basic):
        """Test download with no files"""
        data = swift_data_basic
        data.entries = []
        result = data.download()
        assert result is False

    @patch("swifttools.swift_too.swift.data.os.path.exists")
    @patch("swifttools.swift_too.swift.data.os.path.expanduser")
    @patch("swifttools.swift_too.swift.data.os.path.expandvars")
    @patch("swifttools.swift_too.swift.data.os.path.abspath")
    def test_download_setup(self, mock_abspath, mock_expandvars, mock_expanduser, mock_exists):
        """Test download path setup"""
        mock_abspath.return_value = "/absolute/path"
        mock_expandvars.return_value = "/expanded/path"
        mock_expanduser.return_value = "/user/path"
        mock_exists.return_value = True

        data = SwiftData(obs_id="00012345001", outdir="~/data", autosubmit=False, clobber=True)
        data.entries = [
            SwiftDataFile(filename="test.fits", path="data", url="http://example.com/test.fits", type="BAT")
        ]

        # Mock the download method to avoid actual network calls
        with patch.object(SwiftDataFile, "download", return_value=True):
            result = data.download()
            assert result is True

    def test_post_process_match_filtering(self, swift_data_multiple_entries):
        data = swift_data_multiple_entries
        data.fetch = False
        # single match string
        data.match = "p1/*"
        data._post_process()
        assert len(data.entries) == 2

    def test_post_process_fetch_calls_download(self, swift_data_with_entries):
        data = swift_data_with_entries
        data.fetch = True
        # patch download to ensure it's called
        with patch.object(SwiftData, "download", return_value=True) as mock_download:
            data._post_process()
            assert mock_download.called is True

    def test_download_success_and_quiet_true(self, swift_data_with_entries, tmp_path):
        data = swift_data_with_entries
        data.outdir = str(tmp_path)
        data.quiet = True
        data.clobber = False

        # make download return True

    def test_download_with_clobber_table_length(self, swift_data_basic, tmp_path):
        out = tmp_path / "data"
        out.mkdir()
        file_path = out / "test.fits"
        file_path.write_bytes(b"abc")

        data = swift_data_basic
        data.outdir = str(tmp_path)
        data.entries = [
            SwiftDataFile(filename="test.fits", path="data", url="http://example.com/test.fits", type="BAT")
        ]
        data.clobber = True

        with patch.object(SwiftDataFile, "download", return_value=True):
            _ = data.download()
        _, table = data._table
        assert len(table) == 1  # We have 1 entry

    def test_download_with_clobber_table_path(self, swift_data_basic, tmp_path):
        out = tmp_path / "data"
        out.mkdir()
        file_path = out / "test.fits"
        file_path.write_bytes(b"abc")

        data = swift_data_basic
        data.outdir = str(tmp_path)
        data.entries = [
            SwiftDataFile(filename="test.fits", path="data", url="http://example.com/test.fits", type="BAT")
        ]
        data.clobber = True

        with patch.object(SwiftDataFile, "download", return_value=True):
            _ = data.download()
        _, table = data._table
        assert table[0][0] == "data"  # First file shows full path

    def test_download_existing_file_warns_and_localpath_indexing(
        self, swift_data_basic, swift_data_file_basic, tmp_path
    ):
        out = tmp_path / "data"
        out.mkdir()
        file_path = out / "test.fits"
        file_path.write_bytes(b"abc")

        data = swift_data_basic
        data.outdir = str(tmp_path)
        data.entries = [swift_data_file_basic]
        data.clobber = False
        data.quiet = False
        # index existing files should set localpath
        with pytest.warns(UserWarning, match="test.fits exists and not overwritten"):
            data.download()
        assert data.entries[0].localpath is not None

    def test_download_existing_file_warning(self, swift_data_basic, swift_data_file_basic, tmp_path):
        out = tmp_path / "data"
        out.mkdir()
        file_path = out / "test.fits"
        file_path.write_bytes(b"abc")

        data = swift_data_basic
        data.outdir = str(tmp_path)
        data.entries = [swift_data_file_basic]
        # ensure existing file is detected and warning issued
        data.quiet = False
        data.clobber = False
        # Call download; should warn and not attempt download
        with pytest.warns(UserWarning, match="test.fits exists and not overwritten"):
            data.download()
        # No exception and method completes

    def test_download_file_download_failure(self, swift_data_with_entries, tmp_path):
        data = swift_data_with_entries
        data.outdir = str(tmp_path)

        # Make file_obj.download return False
        with patch.object(SwiftDataFile, "download", return_value=False):
            res = data.download()
            assert res is False
            assert data.status.status == "Rejected"


class TestSwiftDataFile:
    def test_size(self, swift_data_file_basic):
        """Test SwiftDataFile size property"""
        file_obj = swift_data_file_basic
        assert file_obj.size is None

        # Mock localpath
        with tempfile.NamedTemporaryFile(delete=False) as tmp:
            tmp.write(b"test data")
            tmp.flush()  # Ensure data is written
            file_obj.localpath = tmp.name
            assert file_obj.size == len(b"test data")
            os.unlink(tmp.name)

    @patch("swifttools.swift_too.swift.data.os.path.exists")
    def test_download_file(self, mock_exists, swift_data_file_basic, mock_httpx_stream):
        """Test SwiftDataFile download method"""
        mock_exists.return_value = True

        file_obj = swift_data_file_basic

        with patch("builtins.open", create=True):
            result = file_obj.download(outdir="/tmp")
            assert result is True

    def test_download_heasarc_s3_result(self, swift_data_file_heasarc, tmp_path):
        # heasarc url with s3 client download_file called
        file_obj = swift_data_file_heasarc
        s3 = MagicMock()
        # ensure directory creation works
        outdir = str(tmp_path)

        # Call download with s3; should call s3.download_file and return True
        res = file_obj.download(outdir=outdir, s3=s3)
        assert res is True

    def test_download_heasarc_s3_call(self, swift_data_file_heasarc, tmp_path):
        # heasarc url with s3 client download_file called
        file_obj = swift_data_file_heasarc
        s3 = MagicMock()
        # ensure directory creation works
        outdir = str(tmp_path)

        # Call download with s3; should call s3.download_file and return True
        _res = file_obj.download(outdir=outdir, s3=s3)
        # key name should be url replaced prefix
        key_name = file_obj.url.replace("https://heasarc.gsfc.nasa.gov/FTP/", "")
        s3.download_file.assert_called_once_with(
            "nasa-heasarc", key_name, os.path.join(outdir, file_obj.path, file_obj.filename)
        )

    def test_download_http_error(self, swift_data_file_basic, monkeypatch):
        file_obj = swift_data_file_basic

        class FakeResp:
            def __enter__(self):
                return self

            def __exit__(self, *args):
                return False

            def raise_for_status(self):
                raise httpx.HTTPStatusError("error", request=MagicMock(), response=MagicMock())

        monkeypatch.setattr("swifttools.swift_too.swift.data.httpx.stream", lambda *a, **k: FakeResp())

        res = file_obj.download(outdir="/tmp")
        assert res is False

    def test_makedirs_and_s3_download_result(self, swift_data_file_heasarc, monkeypatch, tmp_path):
        # Ensure os.makedirs is called when fulldir doesn't exist and s3 branch used
        file_obj = swift_data_file_heasarc
        s3 = MagicMock()

        # simulate directory doesn't exist
        monkeypatch.setattr("swifttools.swift_too.swift.data.os.path.exists", lambda p: False)
        called = {"makedirs": False}

        def fake_makedirs(p):
            called["makedirs"] = True

        monkeypatch.setattr("swifttools.swift_too.swift.data.os.makedirs", fake_makedirs)

        res = file_obj.download(outdir=str(tmp_path), s3=s3)
        assert res is True

    def test_makedirs_and_s3_download_called(self, swift_data_file_heasarc, monkeypatch, tmp_path):
        # Ensure os.makedirs is called when fulldir doesn't exist and s3 branch used
        file_obj = swift_data_file_heasarc
        s3 = MagicMock()

        # simulate directory doesn't exist
        monkeypatch.setattr("swifttools.swift_too.swift.data.os.path.exists", lambda p: False)
        called = {"makedirs": False}

        def fake_makedirs(p):
            called["makedirs"] = True

        monkeypatch.setattr("swifttools.swift_too.swift.data.os.makedirs", fake_makedirs)

        _res = file_obj.download(outdir=str(tmp_path), s3=s3)
        assert called["makedirs"] is True

    def test_stream_zero_content_length(self, swift_data_file_basic, monkeypatch, tmp_path):
        file_obj = swift_data_file_basic
        outdir = str(tmp_path)

        class FakeResp:
            def __enter__(self):
                return self

            def __exit__(self, *args):
                return False

            def raise_for_status(self):
                return None

            @property
            def headers(self):
                return {}

            def iter_bytes(self, chunk_size=8192):
                yield b"a"
                yield b"b"

        monkeypatch.setattr("swifttools.swift_too.swift.data.httpx.stream", lambda *a, **k: FakeResp())

        res = file_obj.download(outdir=outdir)
        assert res is True
        assert file_obj.localpath is not None


class TestTOOAPIDownloadData:
    def test_download(self, mock_download_data):
        """Test TOOAPIDownloadData download method"""
        mock_obj = mock_download_data
        with patch("swifttools.swift_too.swift.data.SwiftData") as mock_swift_data:
            mock_instance = MagicMock()
            mock_swift_data.return_value = mock_instance
            mock_instance.submit.return_value = None
            mock_instance.fetch = True
            mock_instance.download.return_value = None

            result = mock_obj.download()
            assert result == mock_instance

    def test_download_does_not_call_download_twice_submit_called(self, mock_download_data):
        """TOOAPIDownloadData should not manually call download after submit."""
        mock_obj = mock_download_data
        with patch("swifttools.swift_too.swift.data.SwiftData") as mock_swift_data:
            mock_instance = MagicMock()
            mock_swift_data.return_value = mock_instance
            mock_instance.submit.return_value = None
            mock_instance.fetch = True

            _ = mock_obj.download()

            mock_instance.submit.assert_called_once()

    def test_download_does_not_call_download_twice_download_not_called(self, mock_download_data):
        """TOOAPIDownloadData should not manually call download after submit."""
        mock_obj = mock_download_data
        with patch("swifttools.swift_too.swift.data.SwiftData") as mock_swift_data:
            mock_instance = MagicMock()
            mock_swift_data.return_value = mock_instance
            mock_instance.submit.return_value = None
            mock_instance.fetch = True

            _ = mock_obj.download()

            mock_instance.download.assert_not_called()

    def test_positional_and_anonymous_username(self, mock_download_data):
        class HasObsNoUser(BaseSchema, TOOAPIDownloadData):
            obs_id: str = "000"

        obj = HasObsNoUser()
        with patch("swifttools.swift_too.swift.data.SwiftData") as mock_swift_data:
            inst = MagicMock()
            mock_swift_data.return_value = inst
            # Set up the mock to have _parameters and _local
            inst._parameters = [
                "username",
                "obsid",
                "quicklook",
                "auxil",
                "bat",
                "xrt",
                "uvot",
                "subthresh",
                "log",
                "tdrss",
                "uksdc",
                "itsdc",
            ]
            inst._local = [
                "outdir",
                "clobber",
                "obs_id",
                "targetid",
                "target_id",
                "seg",
                "segment",
                "shared_secret",
                "fetch",
                "match",
                "quiet",
                "aws",
            ]
            inst.fetch = True
            inst.download.return_value = None
            inst.submit.return_value = None

            obj.download("argval")
            mock_swift_data.assert_called_once()
            # positional arg should set obsid on instance
            assert getattr(inst, "obsid") == "argval"

    def test_positional_and_anonymous_username_anonymous(self, mock_download_data):
        class HasObsNoUser(BaseSchema, TOOAPIDownloadData):
            obs_id: str = "000"

        obj = HasObsNoUser()
        with patch("swifttools.swift_too.swift.data.SwiftData") as mock_swift_data:
            inst = MagicMock()
            mock_swift_data.return_value = inst
            # Set up the mock to have _parameters and _local
            inst._parameters = [
                "username",
                "obsid",
                "quicklook",
                "auxil",
                "bat",
                "xrt",
                "uvot",
                "subthresh",
                "log",
                "tdrss",
                "uksdc",
                "itsdc",
            ]
            inst._local = [
                "outdir",
                "clobber",
                "obs_id",
                "targetid",
                "target_id",
                "seg",
                "segment",
                "shared_secret",
                "fetch",
                "match",
                "quiet",
                "aws",
            ]
            inst.fetch = True
            inst.download.return_value = None
            inst.submit.return_value = None

            obj.download("argval")
            # username should be anonymous when not provided
            assert inst.username == "anonymous"

    def test_unexpected_kwarg_raises(self):
        class HasObs(BaseSchema, TOOAPIDownloadData):
            obs_id: str = "000"

        obj = HasObs()
        with pytest.raises(TypeError):
            obj.download(nonexistent=True)

    def test_sets_params_and_username(self):
        class HasObs(BaseSchema, TOOAPIDownloadData):
            obs_id: str = "000"
            username: str = "u"
            shared_secret: str = "s"

        obj = HasObs()
        with patch("swifttools.swift_too.swift.data.SwiftData") as mock_swift_data:
            inst = MagicMock()
            mock_swift_data.return_value = inst
            # Set up the mock to have _parameters and _local
            inst._parameters = [
                "username",
                "obsid",
                "quicklook",
                "auxil",
                "bat",
                "xrt",
                "uvot",
                "subthresh",
                "log",
                "tdrss",
                "uksdc",
                "itsdc",
            ]
            inst._local = [
                "outdir",
                "clobber",
                "obs_id",
                "targetid",
                "target_id",
                "seg",
                "segment",
                "shared_secret",
                "fetch",
                "match",
                "quiet",
                "aws",
            ]
            inst.fetch = True
            inst.download.return_value = None
            inst.submit.return_value = None

            obj.download("argsval", quicklook=True)
            # ensure SwiftData was instantiated and submit/download called
            mock_swift_data.assert_called_once()
            inst.submit.assert_called_once()
