# -*- coding: utf-8 -*-
"""Unit tests for cache updater module.

Tests background cache update logic.
"""

import pytest
from unittest.mock import patch, MagicMock
import pandas as pd
import time


class TestCacheUpdater:
    """Test CacheUpdater class."""

    @pytest.fixture(autouse=True)
    def reset_state(self):
        """Reset module state before each test."""
        import mes_dashboard.core.redis_client as rc
        rc._REDIS_CLIENT = None
        yield
        rc._REDIS_CLIENT = None

    def test_updater_starts_when_redis_enabled(self, reset_state):
        """Test updater starts when Redis is enabled."""
        import mes_dashboard.core.cache_updater as cu

        mock_client = MagicMock()
        mock_client.ping.return_value = True

        with patch.object(cu, 'REDIS_ENABLED', True):
            with patch.object(cu, 'redis_available', return_value=True):
                with patch.object(cu, 'read_sql_df', return_value=None):
                    updater = cu.CacheUpdater(interval=1)
                    try:
                        updater.start()
                        assert updater._is_running is True
                        assert updater._thread is not None
                    finally:
                        updater.stop()
                        time.sleep(0.2)

    def test_updater_does_not_start_when_redis_disabled(self, reset_state):
        """Test updater does not start when Redis is disabled."""
        import mes_dashboard.core.cache_updater as cu

        with patch.object(cu, 'REDIS_ENABLED', False):
            updater = cu.CacheUpdater(interval=1)
            updater.start()
            assert updater._is_running is False

    def test_updater_stops_gracefully(self, reset_state):
        """Test updater stops gracefully."""
        import mes_dashboard.core.cache_updater as cu

        mock_client = MagicMock()
        mock_client.ping.return_value = True

        with patch.object(cu, 'REDIS_ENABLED', True):
            with patch.object(cu, 'redis_available', return_value=True):
                with patch.object(cu, 'read_sql_df', return_value=None):
                    updater = cu.CacheUpdater(interval=1)
                    updater.start()
                    assert updater._is_running is True

                    updater.stop()
                    time.sleep(0.2)  # Give thread time to stop
                    assert updater._is_running is False


class TestCheckSysDate:
    """Test SYS_DATE checking logic."""

    def test_check_sys_date_returns_value(self):
        """Test _check_sys_date returns correct value."""
        import mes_dashboard.core.cache_updater as cu

        mock_df = pd.DataFrame({'SYS_DATE': ['2024-01-15 10:30:00']})

        with patch.object(cu, 'read_sql_df', return_value=mock_df):
            updater = cu.CacheUpdater()
            result = updater._check_sys_date()
            assert result == '2024-01-15 10:30:00'

    def test_check_sys_date_handles_empty_result(self):
        """Test _check_sys_date handles empty result."""
        import mes_dashboard.core.cache_updater as cu

        with patch.object(cu, 'read_sql_df', return_value=pd.DataFrame()):
            updater = cu.CacheUpdater()
            result = updater._check_sys_date()
            assert result is None

    def test_check_sys_date_handles_none_result(self):
        """Test _check_sys_date handles None result."""
        import mes_dashboard.core.cache_updater as cu

        with patch.object(cu, 'read_sql_df', return_value=None):
            updater = cu.CacheUpdater()
            result = updater._check_sys_date()
            assert result is None

    def test_check_sys_date_handles_exception(self):
        """Test _check_sys_date handles database exception."""
        import mes_dashboard.core.cache_updater as cu

        with patch.object(cu, 'read_sql_df', side_effect=Exception("Database error")):
            updater = cu.CacheUpdater()
            result = updater._check_sys_date()
            assert result is None


class TestLoadFullTable:
    """Test full table loading logic."""

    def test_load_full_table_success(self):
        """Test _load_full_table loads data correctly."""
        import mes_dashboard.core.cache_updater as cu

        test_df = pd.DataFrame({
            'LOTID': ['LOT001', 'LOT002'],
            'QTY': [100, 200],
            'WORKORDER': ['WO001', 'WO002']
        })

        with patch.object(cu, 'read_sql_df', return_value=test_df):
            updater = cu.CacheUpdater()
            result = updater._load_full_table()

            assert result is not None
            assert len(result) == 2

    def test_load_full_table_handles_none(self):
        """Test _load_full_table handles None result."""
        import mes_dashboard.core.cache_updater as cu

        with patch.object(cu, 'read_sql_df', return_value=None):
            updater = cu.CacheUpdater()
            result = updater._load_full_table()
            assert result is None

    def test_load_full_table_handles_exception(self):
        """Test _load_full_table handles exception."""
        import mes_dashboard.core.cache_updater as cu

        with patch.object(cu, 'read_sql_df', side_effect=Exception("Database error")):
            updater = cu.CacheUpdater()
            result = updater._load_full_table()
            assert result is None


class TestUpdateRedisCache:
    """Test Redis cache update logic."""

    def test_update_redis_cache_success(self):
        """Test _update_redis_cache updates cache correctly."""
        import mes_dashboard.core.cache_updater as cu

        mock_client = MagicMock()
        mock_pipeline = MagicMock()
        mock_client.pipeline.return_value = mock_pipeline

        test_df = pd.DataFrame({
            'LOTID': ['LOT001'],
            'QTY': [100]
        })

        with patch.object(cu, 'get_redis_client', return_value=mock_client):
            with patch.object(cu, 'get_key', side_effect=lambda k: f'mes_wip:{k}'):
                updater = cu.CacheUpdater()
                result = updater._update_redis_cache(test_df, '2024-01-15 10:30:00')

                assert result is True
                mock_pipeline.rename.assert_called_once()
                mock_pipeline.execute.assert_called_once()
                assert mock_pipeline.set.call_count == 3
                for call in mock_pipeline.set.call_args_list:
                    assert call.kwargs.get("ex") == updater.interval * 3

    def test_update_redis_cache_no_client(self):
        """Test _update_redis_cache handles no client."""
        import mes_dashboard.core.cache_updater as cu

        test_df = pd.DataFrame({'LOTID': ['LOT001']})

        with patch.object(cu, 'get_redis_client', return_value=None):
            updater = cu.CacheUpdater()
            result = updater._update_redis_cache(test_df, '2024-01-15')
            assert result is False

    def test_update_redis_cache_cleans_staging_key_on_failure(self):
        """Failed publish should clean staged key and keep function safe."""
        import mes_dashboard.core.cache_updater as cu

        mock_client = MagicMock()
        mock_pipeline = MagicMock()
        mock_pipeline.execute.side_effect = RuntimeError("pipeline failed")
        mock_client.pipeline.return_value = mock_pipeline

        test_df = pd.DataFrame({'LOTID': ['LOT001'], 'QTY': [100]})

        with patch.object(cu, 'get_redis_client', return_value=mock_client):
            with patch.object(cu, 'get_key', side_effect=lambda k: f'mes_wip:{k}'):
                updater = cu.CacheUpdater()
                result = updater._update_redis_cache(test_df, '2024-01-15 10:30:00')

        assert result is False
        mock_client.delete.assert_called_once()
        staged_key = mock_client.delete.call_args.args[0]
        assert "staging" in staged_key

    def test_update_redis_cache_ttl_override(self):
        """Configured TTL override should apply to all Redis keys."""
        import mes_dashboard.core.cache_updater as cu

        mock_client = MagicMock()
        mock_pipeline = MagicMock()
        mock_client.pipeline.return_value = mock_pipeline
        test_df = pd.DataFrame({'LOTID': ['LOT001'], 'QTY': [100]})

        with patch.object(cu, 'WIP_CACHE_TTL_SECONDS', 42):
            with patch.object(cu, 'get_redis_client', return_value=mock_client):
                with patch.object(cu, 'get_key', side_effect=lambda k: f'mes_wip:{k}'):
                    updater = cu.CacheUpdater(interval=600)
                    result = updater._update_redis_cache(test_df, '2024-01-15 10:30:00')

        assert result is True
        assert mock_pipeline.set.call_count == 3
        for call in mock_pipeline.set.call_args_list:
            assert call.kwargs.get("ex") == 42


class TestCacheUpdateFlow:
    """Test complete cache update flow."""

    def test_no_update_when_sys_date_unchanged(self):
        """Test cache doesn't update when SYS_DATE unchanged."""
        import mes_dashboard.core.cache_updater as cu

        mock_df = pd.DataFrame({'SYS_DATE': ['2024-01-15 10:30:00']})
        mock_client = MagicMock()
        mock_client.get.return_value = '2024-01-15 10:30:00'

        with patch.object(cu, 'read_sql_df', return_value=mock_df):
            with patch.object(cu, 'redis_available', return_value=True):
                with patch.object(cu, 'get_redis_client', return_value=mock_client):
                    with patch.object(cu, 'get_key', side_effect=lambda k: f'mes_wip:{k}'):
                        updater = cu.CacheUpdater()
                        # Simulate already having cached the same date
                        result = updater._check_and_update(force=False)
                        # No update because dates match
                        assert result is False

    def test_update_when_sys_date_changes(self):
        """Test cache updates when SYS_DATE changes."""
        import mes_dashboard.core.cache_updater as cu

        updater = cu.CacheUpdater()

        mock_df = pd.DataFrame({'SYS_DATE': ['2024-01-15 11:00:00']})

        with patch.object(cu, 'read_sql_df', return_value=mock_df):
            current_date = updater._check_sys_date()
            old_date = '2024-01-15 10:30:00'
            needs_update = current_date != old_date

            assert needs_update is True
