"""Integration tests for mldash — end-to-end save() and export() flows."""
import json
import os
import zipfile

import joblib
import numpy as np
import pytest
from unittest.mock import MagicMock, patch

import mldash
from mldash._errors import MLDashUploadError, MLDashValidationError


class TestSave:
    """Test the offline save() flow (no network)."""

    def test_save_classifier(self, fitted_rf_classifier, tmp_path):
        out = str(tmp_path / 'model.zip')
        result = mldash.save(
            model=fitted_rf_classifier,
            name='RF Classifier',
            path=out,
            task_type='classification',
            features={'feat_a': 'numeric', 'feat_b': 'numeric', 'feat_c': 'numeric'},
            target={'label': 'categorical'},
        )
        assert result == out
        assert os.path.isfile(out)

        # Verify ZIP contents
        with zipfile.ZipFile(out, 'r') as zf:
            names = zf.namelist()
            assert 'metadata.json' in names
            assert 'model.pkl' in names

            meta = json.loads(zf.read('metadata.json'))
            assert meta['name'] == 'RF Classifier'
            assert meta['task_type'] == 'classification'
            assert meta['framework'] == 'sklearn'
            assert meta['model_class'] == 'RandomForestClassifier'
            assert meta['features'] == {'feat_a': 'numeric', 'feat_b': 'numeric', 'feat_c': 'numeric'}

    def test_save_regressor_with_scaler(self, fitted_regressor, fitted_scaler, tmp_path):
        out = str(tmp_path / 'reg.zip')
        result = mldash.save(
            model=fitted_regressor,
            name='Ridge Regressor',
            path=out,
            scaler=fitted_scaler,
            description='A test regressor',
        )
        assert os.path.isfile(result)

        with zipfile.ZipFile(result, 'r') as zf:
            names = zf.namelist()
            assert 'model.pkl' in names
            assert 'scaler.pkl' in names
            assert 'encoder.pkl' not in names

            meta = json.loads(zf.read('metadata.json'))
            assert meta['name'] == 'Ridge Regressor'
            assert meta['description'] == 'A test regressor'
            assert meta['task_type'] == 'regression'
            assert meta['has_scaler'] is True
            assert meta['has_encoder'] is False

    def test_save_with_all_artifacts(self, fitted_classifier, fitted_scaler,
                                     fitted_encoder, sample_label_map, tmp_path):
        out = str(tmp_path / 'full.zip')
        mldash.save(
            model=fitted_classifier,
            name='Full Model',
            path=out,
            scaler=fitted_scaler,
            encoder=fitted_encoder,
            label_map=sample_label_map,
        )

        with zipfile.ZipFile(out, 'r') as zf:
            names = zf.namelist()
            assert 'model.pkl' in names
            assert 'scaler.pkl' in names
            assert 'encoder.pkl' in names
            assert 'label_map.pkl' in names
            assert 'metadata.json' in names

    def test_save_model_is_loadable(self, fitted_classifier, tmp_path):
        out = str(tmp_path / 'loadable.zip')
        mldash.save(model=fitted_classifier, name='Loadable', path=out)

        with zipfile.ZipFile(out, 'r') as zf:
            import io
            model_bytes = zf.read('model.pkl')
            loaded = joblib.load(io.BytesIO(model_bytes))
            assert type(loaded).__name__ == 'LogisticRegression'
            # Model should be functional
            pred = loaded.predict(np.array([[1, 2]]))
            assert len(pred) == 1

    def test_save_validates_inputs(self, tmp_path):
        with pytest.raises(MLDashValidationError, match="predict"):
            mldash.save(model=object(), name='Bad', path=str(tmp_path / 'bad.zip'))

    def test_save_validates_name(self, fitted_classifier, tmp_path):
        with pytest.raises(MLDashValidationError, match="name"):
            mldash.save(model=fitted_classifier, name='', path=str(tmp_path / 'bad.zip'))

    def test_save_validates_features(self, fitted_classifier, tmp_path):
        with pytest.raises(MLDashValidationError, match="numeric"):
            mldash.save(
                model=fitted_classifier, name='Test',
                path=str(tmp_path / 'bad.zip'),
                features={'col': 'invalid_type'},
            )

    def test_save_cleans_up_temp(self, fitted_classifier, tmp_path):
        import tempfile
        before = set(os.listdir(tempfile.gettempdir()))
        mldash.save(model=fitted_classifier, name='Cleanup', path=str(tmp_path / 'out.zip'))
        after = set(os.listdir(tempfile.gettempdir()))
        # No new mldash_ temp dirs should remain
        new_dirs = after - before
        assert not any(d.startswith('mldash_') for d in new_dirs)


class TestExport:
    """Test the export() flow with mocked upload."""

    @patch('mldash._upload.requests.post')
    def test_export_full_flow(self, mock_post, fitted_rf_classifier, creds_dir):
        # Set up credentials
        mldash._session['url'] = 'http://localhost:8000'
        mldash._session['api_key'] = 'mldash_test_key'

        mock_resp = MagicMock()
        mock_resp.status_code = 201
        mock_resp.json.return_value = {
            'model': {'id': 99, 'name': 'Exported', 'slug': 'exported'},
            'auto_detected': {'task_type': 'classification'},
        }
        mock_post.return_value = mock_resp

        try:
            result = mldash.export(
                model=fitted_rf_classifier,
                name='Exported',
                description='E2E test',
                task_type='classification',
                features={'feat_a': 'numeric'},
                target={'label': 'categorical'},
            )

            assert result['model_id'] == 99
            assert result['model_name'] == 'Exported'
            assert result['model_slug'] == 'exported'
            assert result['url'] == 'http://localhost:8000'

            # Verify the upload was called correctly
            mock_post.assert_called_once()
            call_kwargs = mock_post.call_args
            assert call_kwargs.kwargs['headers'] == {'X-API-Key': 'mldash_test_key'}
            assert call_kwargs.kwargs['data']['name'] == 'Exported'
        finally:
            mldash._session['url'] = None
            mldash._session['api_key'] = None

    @patch('mldash._upload.requests.post')
    def test_export_with_project_id(self, mock_post, fitted_classifier, creds_dir):
        mldash._session['url'] = 'http://localhost:8000'
        mldash._session['api_key'] = 'mldash_test_key'

        mock_resp = MagicMock()
        mock_resp.status_code = 201
        mock_resp.json.return_value = {'model': {'id': 1, 'name': 'M', 'slug': 'm'}}
        mock_post.return_value = mock_resp

        try:
            mldash.export(model=fitted_classifier, name='M', project=42)
            data = mock_post.call_args.kwargs['data']
            assert data['project'] == '42'
        finally:
            mldash._session['url'] = None
            mldash._session['api_key'] = None

    def test_export_fails_without_credentials(self, fitted_classifier, creds_dir):
        mldash._session['url'] = None
        mldash._session['api_key'] = None
        # Clear env vars too
        old_url = os.environ.pop('MLDASH_URL', None)
        old_key = os.environ.pop('MLDASH_API_KEY', None)
        try:
            with pytest.raises(MLDashValidationError, match="Not logged in"):
                mldash.export(model=fitted_classifier, name='Test')
        finally:
            if old_url:
                os.environ['MLDASH_URL'] = old_url
            if old_key:
                os.environ['MLDASH_API_KEY'] = old_key

    @patch('mldash._upload.requests.post')
    def test_export_cleans_up_on_success(self, mock_post, fitted_classifier, creds_dir):
        mldash._session['url'] = 'http://localhost:8000'
        mldash._session['api_key'] = 'mldash_test_key'

        mock_resp = MagicMock()
        mock_resp.status_code = 201
        mock_resp.json.return_value = {'model': {'id': 1, 'name': 'M', 'slug': 'm'}}
        mock_post.return_value = mock_resp

        import tempfile
        before = set(os.listdir(tempfile.gettempdir()))
        try:
            mldash.export(model=fitted_classifier, name='Cleanup Test')
        finally:
            mldash._session['url'] = None
            mldash._session['api_key'] = None
        after = set(os.listdir(tempfile.gettempdir()))
        new_dirs = after - before
        assert not any(d.startswith('mldash_') for d in new_dirs)

    @patch('mldash._upload.requests.post')
    def test_export_cleans_up_on_failure(self, mock_post, fitted_classifier, creds_dir):
        mldash._session['url'] = 'http://localhost:8000'
        mldash._session['api_key'] = 'mldash_test_key'

        import requests as req
        mock_post.side_effect = req.ConnectionError('refused')

        import tempfile
        before = set(os.listdir(tempfile.gettempdir()))
        try:
            with pytest.raises(MLDashUploadError):
                mldash.export(model=fitted_classifier, name='Fail Test')
        finally:
            mldash._session['url'] = None
            mldash._session['api_key'] = None
        after = set(os.listdir(tempfile.gettempdir()))
        new_dirs = after - before
        assert not any(d.startswith('mldash_') for d in new_dirs)


class TestCredentialResolution:
    """Test the fallback chain for URL and API key resolution."""

    def test_session_takes_priority(self, creds_dir):
        from mldash._credentials import store
        store(url='http://stored.example.com')
        mldash._session['url'] = 'http://session.example.com'
        try:
            assert mldash._resolve_url(None) == 'http://session.example.com'
        finally:
            mldash._session['url'] = None

    def test_explicit_takes_priority_over_session(self, creds_dir):
        mldash._session['url'] = 'http://session.example.com'
        try:
            assert mldash._resolve_url('http://explicit.example.com') == 'http://explicit.example.com'
        finally:
            mldash._session['url'] = None

    def test_stored_credentials_used(self, creds_dir):
        from mldash._credentials import store
        store(url='http://stored.example.com', api_key='mldash_stored')
        mldash._session['url'] = None
        mldash._session['api_key'] = None

        assert mldash._resolve_url(None) == 'http://stored.example.com'
        assert mldash._resolve_api_key(None) == 'mldash_stored'

    def test_env_var_used(self, creds_dir):
        mldash._session['url'] = None
        os.environ['MLDASH_URL'] = 'http://env.example.com'
        try:
            assert mldash._resolve_url(None) == 'http://env.example.com'
        finally:
            del os.environ['MLDASH_URL']

    def test_raises_when_no_credentials(self, creds_dir):
        mldash._session['url'] = None
        mldash._session['api_key'] = None
        old_url = os.environ.pop('MLDASH_URL', None)
        old_key = os.environ.pop('MLDASH_API_KEY', None)
        try:
            with pytest.raises(MLDashValidationError, match="Not logged in"):
                mldash._resolve_url(None)
            with pytest.raises(MLDashValidationError, match="Not logged in"):
                mldash._resolve_api_key(None)
        finally:
            if old_url:
                os.environ['MLDASH_URL'] = old_url
            if old_key:
                os.environ['MLDASH_API_KEY'] = old_key
