"""Tests for factory functions."""

import pytest
import numpy as np
from neoplot import timeseries, scatter, map, Style, load_theme


class TestFactories:
    """Test factory functions."""
    
    def test_timeseries_factory(self):
        """Test creating a timeseries figure."""
        style = Style(dpi=96)  # Use lower DPI for testing
        fig, ax = timeseries(style=style)
        
        assert fig is not None
        assert ax is not None
        assert hasattr(ax, 'plot')
    
    def test_timeseries_with_theme_string(self):
        """Test timeseries with theme name string."""
        try:
            fig, ax = timeseries(style='publication/paper')
            assert fig is not None
            assert ax is not None
        except FileNotFoundError:
            pytest.skip("Theme file not found")
    
    def test_scatter_factory(self):
        """Test creating a scatter figure."""
        style = Style(dpi=96)
        fig, ax = scatter(style=style)
        
        assert fig is not None
        assert ax is not None
        assert hasattr(ax, 'plot')
    
    def test_map_factory(self):
        """Test creating a map figure."""
        style = Style(dpi=96)
        fig, ax = map(style=style, projection='Mercator')
        
        assert fig is not None
        assert ax is not None
        assert hasattr(ax, 'plot')
        # Ensure underlying axis is a GeoAxes-like object
        assert hasattr(ax.ax, 'coastlines')
    
    def test_map_with_invalid_projection(self):
        """Test map with invalid projection raises error."""
        style = Style(dpi=96)
        with pytest.raises(ValueError):
            fig, ax = map(style=style, projection='InvalidProjection')

    def test_map_plot_tuple_data_and_finalize(self):
        """Map axis can plot (lons, lats) tuple data and finalize safely."""
        style = Style(dpi=96)
        fig, ax = map(style=style, projection='Mercator')

        lons = np.array([-120.0, -60.0, 0.0, 60.0, 120.0])
        lats = np.array([30.0, 20.0, 0.0, -20.0, -30.0])
        ax.plot((lons, lats), label='transect')
        ax.finalize()

        assert len(ax.ax.lines) >= 1
        # GeoAxes should have map artists (coastlines/borders) beyond plotted line
        assert hasattr(ax.ax, 'collections')

    def test_map_with_projection_kwargs(self):
        """Map accepts projection_kwargs for parametric projections like RotatedPole."""
        style = Style(dpi=96)
        try:
            import cartopy.crs as ccrs
            # RotatedPole requires pole_latitude and pole_longitude
            fig, ax = map(
                style=style,
                projection='RotatedPole',
                projection_kwargs={'pole_latitude': 39.25, 'pole_longitude': -162.0}
            )
            assert fig is not None
            assert ax is not None
            assert hasattr(ax.geo_ax, 'set_global')
        except (ValueError, AttributeError) as e:
            pytest.skip(f"RotatedPole projection not available: {e}")

    def test_map_with_extent_and_extent_crs(self):
        """Map accepts explicit extent and extent_crs instead of global view."""
        style = Style(dpi=96)
        try:
            import cartopy.crs as ccrs
            extent = [-25, 45, 30, 72]  # EU CORDEX approximate domain
            extent_crs = ccrs.PlateCarree()
            fig, ax = map(
                style=style,
                projection='Mercator',
                extent=extent,
                extent_crs=extent_crs
            )
            assert fig is not None
            assert ax is not None
            # Verify extent is not global by checking axis limits are bounded
            assert hasattr(ax.geo_ax, 'get_xlim')
        except Exception as e:
            pytest.skip(f"extent/extent_crs test failed: {e}")

    def test_map_extent_crs_mismatch_raises(self):
        """Map raises ValueError if extent and extent_crs are not both provided."""
        style = Style(dpi=96)
        # Extent without extent_crs
        with pytest.raises(ValueError, match="Both 'extent' and 'extent_crs'"):
            map(style=style, extent=[-25, 45, 30, 72])
        
        # extent_crs without extent
        import cartopy.crs as ccrs
        with pytest.raises(ValueError, match="Both 'extent' and 'extent_crs'"):
            map(style=style, extent_crs=ccrs.PlateCarree())

    def test_map_with_default_data_crs(self):
        """Map accepts default_data_crs for plot() transform fallback."""
        style = Style(dpi=96)
        try:
            import cartopy.crs as ccrs
            default_crs = ccrs.RotatedPole(pole_latitude=39.25, pole_longitude=-162.0)
            fig, ax = map(
                style=style,
                projection='Mercator',
                default_data_crs=default_crs
            )
            assert fig is not None
            assert ax is not None
            assert ax.default_data_crs is default_crs
        except Exception as e:
            pytest.skip(f"default_data_crs test failed: {e}")

    def test_map_plot_uses_default_data_crs(self):
        """Map.plot() uses default_data_crs when transform argument is omitted."""
        style = Style(dpi=96)
        try:
            import cartopy.crs as ccrs
            import numpy as np
            default_crs = ccrs.PlateCarree()
            fig, ax = map(
                style=style,
                projection='Mercator',
                default_data_crs=default_crs
            )
            # Plot without explicit transform; should use default_data_crs
            lons = np.array([0.0, 10.0, 20.0])
            lats = np.array([40.0, 45.0, 50.0])
            ax.plot((lons, lats), label='test')
            ax.finalize()
            assert len(ax.ax.lines) >= 1
        except Exception as e:
            pytest.skip(f"plot default_data_crs test failed: {e}")

    def test_map_rotated_pole_full_workflow(self):
        """Test EU CORDEX 3km-like rotated pole setup."""
        style = Style(dpi=96)
        try:
            import cartopy.crs as ccrs
            import numpy as np
            # Approximate EU CORDEX 3km parameters (pole and extent)
            pole_lat = 39.25
            pole_lon = -162.0
            fig, ax = map(
                style=style,
                projection='RotatedPole',
                projection_kwargs={'pole_latitude': pole_lat, 'pole_longitude': pole_lon},
                extent=[-25, 45, 30, 72],
                extent_crs=ccrs.PlateCarree(),
                default_data_crs=ccrs.RotatedPole(pole_latitude=pole_lat, pole_longitude=pole_lon)
            )
            # Add some dummy rotated-grid data (in rotated coordinates)
            rlons = np.array([10.0, 20.0, 30.0])
            rlats = np.array([20.0, 25.0, 30.0])
            ax.plot((rlons, rlats), label='rotated_data')
            ax.finalize()
            assert fig is not None
            assert len(ax.ax.lines) >= 1
        except Exception as e:
            pytest.skip(f"EU CORDEX rotated pole test failed: {e}")

    def test_map_gridline_label_defaults_left_bottom_only(self):
        """Map defaults to left/bottom labels only, outside map lines."""
        style = Style(dpi=96)
        fig, ax = map(style=style, projection='Mercator')

        gl = ax.gridliner
        assert gl is not None
        assert gl.left_labels is True
        assert gl.right_labels is False
        assert gl.top_labels is False
        assert gl.bottom_labels is True
        assert gl.x_inline is False
        assert gl.y_inline is False

    def test_map_gridline_label_sides_are_pluggable(self):
        """Map supports overriding default gridline label side toggles."""
        style = Style(dpi=96)
        fig, ax = map(
            style=style,
            projection='Mercator',
            gridline_left_labels=False,
            gridline_right_labels=False,
            gridline_bottom_labels=True,
        )

        gl = ax.gridliner
        assert gl is not None
        assert gl.left_labels is False
        assert gl.right_labels is False
        assert gl.bottom_labels is True
    
    def test_figsize_override(self):
        """Test overriding figure size."""
        style = Style(figsize=(10, 8), dpi=96)
        fig, ax = timeseries(style=style, figsize=(12, 10))
        
        # Figure size should be from override, not style
        assert fig.get_size_inches()[0] == 12
        assert fig.get_size_inches()[1] == 10

    def test_timeseries_draws_default_zero_line(self):
        """Timeseries finalization draws dashed y=0 line by default."""
        style = Style(dpi=96)
        fig, ax = timeseries(style=style)
        data = np.array([1.0, -1.0, 2.0])
        ax.plot(data, times=np.arange(len(data)))
        ax.finalize()

        zero_lines = [
            line for line in ax.ax.lines
            if getattr(line, "get_gid", lambda: None)() == "neoplot_zero_line"
        ]
        assert len(zero_lines) >= 1
        assert zero_lines[0].get_alpha() == pytest.approx(0.5)

    def test_timeseries_zero_line_can_be_disabled(self):
        """y=0 line is omitted when zero_line_visible is False."""
        style = Style(dpi=96, zero_line_visible=False)
        fig, ax = timeseries(style=style)
        data = np.array([1.0, -1.0, 2.0])
        ax.plot(data, times=np.arange(len(data)))
        ax.finalize()

        zero_lines = [
            line for line in ax.ax.lines
            if getattr(line, "get_gid", lambda: None)() == "neoplot_zero_line"
        ]
        assert len(zero_lines) == 0

    def test_map_does_not_draw_default_zero_line(self):
        """MapAxis skips the default y=0 line."""
        style = Style(dpi=96)
        fig, ax = map(style=style, projection='Mercator')
        ax.plot([0, 1, 2])
        ax.finalize()

        zero_lines = [
            line for line in ax.ax.lines
            if getattr(line, "get_gid", lambda: None)() == "neoplot_zero_line"
        ]
        assert len(zero_lines) == 0

    def test_timeseries_draws_left_and_bottom_baselines(self):
        """When spines are hidden, custom left/bottom baselines are drawn."""
        style = Style(dpi=96, spines_visible=False, axis_baselines_visible=True)
        fig, ax = timeseries(style=style)
        data = np.array([1.0, -1.0, 2.0])
        ax.plot(data, times=np.arange(len(data)))
        ax.finalize()

        baseline_lines = [
            line for line in ax.ax.lines
            if getattr(line, "get_gid", lambda: None)() == "neoplot_axis_baseline"
        ]
        assert len(baseline_lines) == 2
        assert baseline_lines[0].get_alpha() == pytest.approx(0.5)
        assert baseline_lines[1].get_alpha() == pytest.approx(0.5)

    def test_timeseries_baselines_can_be_disabled(self):
        """Custom baselines are omitted when axis_baselines_visible is False."""
        style = Style(dpi=96, spines_visible=False, axis_baselines_visible=False)
        fig, ax = timeseries(style=style)
        data = np.array([1.0, -1.0, 2.0])
        ax.plot(data, times=np.arange(len(data)))
        ax.finalize()

        baseline_lines = [
            line for line in ax.ax.lines
            if getattr(line, "get_gid", lambda: None)() == "neoplot_axis_baseline"
        ]
        assert len(baseline_lines) == 0

    def test_map_does_not_draw_axis_baselines(self):
        """MapAxis skips custom left/bottom baselines."""
        style = Style(dpi=96, spines_visible=False, axis_baselines_visible=True)
        fig, ax = map(style=style, projection='Mercator')
        ax.plot([0, 1, 2])
        ax.finalize()

        baseline_lines = [
            line for line in ax.ax.lines
            if getattr(line, "get_gid", lambda: None)() == "neoplot_axis_baseline"
        ]
        assert len(baseline_lines) == 0
