# -*- coding: utf-8 -*-
"""Unit tests for Excel query service functions.

Tests the core service functions without database dependencies.
"""

import pytest
from unittest.mock import MagicMock, patch
from mes_dashboard.services.excel_query_service import (
    parse_excel,
    get_column_unique_values,
    execute_batch_query,
    execute_advanced_batch_query,
    detect_excel_column_type,
    escape_like_pattern,
    build_like_condition,
    build_date_range_condition,
    validate_like_keywords,
    sanitize_column_name,
    validate_table_name,
    LIKE_KEYWORD_LIMIT,
    PARSE_ERROR_MESSAGE,
    COLUMN_READ_ERROR_MESSAGE,
    QUERY_ERROR_MESSAGE,
)


class TestDetectExcelColumnType:
    """Tests for detect_excel_column_type function."""

    def test_empty_values_returns_text(self):
        """Empty list should return text type."""
        result = detect_excel_column_type([])
        assert result['detected_type'] == 'text'
        assert result['type_label'] == '文字'

    def test_detect_date_type(self):
        """Should detect date format YYYY-MM-DD."""
        values = ['2024-01-15', '2024-02-20', '2024-03-25', '2024-04-30']
        result = detect_excel_column_type(values)
        assert result['detected_type'] == 'date'
        assert result['type_label'] == '日期'

    def test_detect_date_with_slash(self):
        """Should detect date format YYYY/MM/DD."""
        values = ['2024/01/15', '2024/02/20', '2024/03/25', '2024/04/30']
        result = detect_excel_column_type(values)
        assert result['detected_type'] == 'date'
        assert result['type_label'] == '日期'

    def test_detect_datetime_type(self):
        """Should detect datetime format."""
        values = [
            '2024-01-15 10:30:00',
            '2024-02-20 14:45:30',
            '2024-03-25T08:00:00',
            '2024-04-30 23:59:59'
        ]
        result = detect_excel_column_type(values)
        assert result['detected_type'] == 'datetime'
        assert result['type_label'] == '日期時間'

    def test_detect_number_type(self):
        """Should detect numeric values."""
        values = ['123', '456.78', '-99', '0', '1000000']
        result = detect_excel_column_type(values)
        assert result['detected_type'] == 'number'
        assert result['type_label'] == '數值'

    def test_detect_id_type(self):
        """Should detect ID pattern (uppercase alphanumeric)."""
        values = ['LOT001', 'WIP-2024-001', 'ABC_123', 'PROD001', 'TEST_ID']
        result = detect_excel_column_type(values)
        assert result['detected_type'] == 'id'
        assert result['type_label'] == '識別碼'

    def test_mixed_values_returns_text(self):
        """Mixed values should return text type."""
        values = ['abc', '123', '2024-01-01', 'xyz', 'test']
        result = detect_excel_column_type(values)
        assert result['detected_type'] == 'text'
        assert result['type_label'] == '文字'

    def test_sample_values_included(self):
        """Should include sample values in result."""
        values = ['A', 'B', 'C', 'D', 'E', 'F']
        result = detect_excel_column_type(values)
        assert 'sample_values' in result
        assert len(result['sample_values']) <= 5


class TestEscapeLikePattern:
    """Tests for escape_like_pattern function."""

    def test_escape_percent(self):
        """Should escape percent sign."""
        assert escape_like_pattern('100%') == '100\\%'

    def test_escape_underscore(self):
        """Should escape underscore."""
        assert escape_like_pattern('test_value') == 'test\\_value'

    def test_escape_backslash(self):
        """Should escape backslash."""
        assert escape_like_pattern('path\\file') == 'path\\\\file'

    def test_escape_multiple_specials(self):
        """Should escape multiple special characters."""
        assert escape_like_pattern('50%_off') == '50\\%\\_off'

    def test_no_escape_needed(self):
        """Should return unchanged if no special chars."""
        assert escape_like_pattern('normalvalue') == 'normalvalue'


class TestBuildLikeCondition:
    """Tests for build_like_condition function."""

    def test_contains_mode(self):
        """Should build LIKE %...% pattern."""
        condition, params = build_like_condition('COL', ['abc'], 'contains')
        assert 'LIKE :like_0' in condition
        assert params['like_0'] == '%abc%'

    def test_prefix_mode(self):
        """Should build LIKE ...% pattern."""
        condition, params = build_like_condition('COL', ['abc'], 'prefix')
        assert 'LIKE :like_0' in condition
        assert params['like_0'] == 'abc%'

    def test_suffix_mode(self):
        """Should build LIKE %... pattern."""
        condition, params = build_like_condition('COL', ['abc'], 'suffix')
        assert 'LIKE :like_0' in condition
        assert params['like_0'] == '%abc'

    def test_multiple_values(self):
        """Should build OR conditions for multiple values."""
        condition, params = build_like_condition('COL', ['a', 'b', 'c'], 'contains')
        assert 'OR' in condition
        assert len(params) == 3
        assert params['like_0'] == '%a%'
        assert params['like_1'] == '%b%'
        assert params['like_2'] == '%c%'

    def test_empty_values(self):
        """Should return empty for empty values."""
        condition, params = build_like_condition('COL', [], 'contains')
        assert condition == ''
        assert params == {}

    def test_escape_clause_included(self):
        """Should include ESCAPE clause."""
        condition, params = build_like_condition('COL', ['test'], 'contains')
        assert "ESCAPE '\\')" in condition


class TestBuildDateRangeCondition:
    """Tests for build_date_range_condition function."""

    def test_both_dates(self):
        """Should build condition with both dates."""
        condition, params = build_date_range_condition(
            'TXNDATE', '2024-01-01', '2024-12-31'
        )
        assert 'TO_DATE(:date_from' in condition
        assert 'TO_DATE(:date_to' in condition
        assert params['date_from'] == '2024-01-01'
        assert params['date_to'] == '2024-12-31'

    def test_only_from_date(self):
        """Should build condition with only start date."""
        condition, params = build_date_range_condition(
            'TXNDATE', date_from='2024-01-01'
        )
        assert '>=' in condition
        assert 'date_from' in params
        assert 'date_to' not in params

    def test_only_to_date(self):
        """Should build condition with only end date."""
        condition, params = build_date_range_condition(
            'TXNDATE', date_to='2024-12-31'
        )
        assert '<' in condition
        assert 'date_to' in params
        assert 'date_from' not in params

    def test_no_dates(self):
        """Should return empty for no dates."""
        condition, params = build_date_range_condition('TXNDATE')
        assert condition == ''
        assert params == {}

    def test_end_date_includes_full_day(self):
        """End date condition should include +1 for full day."""
        condition, params = build_date_range_condition(
            'TXNDATE', date_to='2024-12-31'
        )
        assert '+ 1' in condition


class TestValidateLikeKeywords:
    """Tests for validate_like_keywords function."""

    def test_within_limit(self):
        """Should pass validation for values within limit."""
        values = ['a'] * 50
        result = validate_like_keywords(values)
        assert result['valid'] is True

    def test_at_limit(self):
        """Should pass validation at exact limit."""
        values = ['a'] * LIKE_KEYWORD_LIMIT
        result = validate_like_keywords(values)
        assert result['valid'] is True

    def test_exceeds_limit(self):
        """Should fail validation when exceeding limit."""
        values = ['a'] * (LIKE_KEYWORD_LIMIT + 1)
        result = validate_like_keywords(values)
        assert result['valid'] is False
        assert 'error' in result


class TestSanitizeColumnName:
    """Tests for sanitize_column_name function."""

    def test_valid_name(self):
        """Should keep valid column name."""
        assert sanitize_column_name('LOT_ID') == 'LOT_ID'

    def test_removes_special_chars(self):
        """Should remove special characters."""
        assert sanitize_column_name('LOT-ID') == 'LOTID'
        assert sanitize_column_name('LOT ID') == 'LOTID'

    def test_allows_underscore(self):
        """Should allow underscore."""
        assert sanitize_column_name('MY_COLUMN_NAME') == 'MY_COLUMN_NAME'

    def test_prevents_sql_injection(self):
        """Should prevent SQL injection attempts."""
        assert sanitize_column_name("COL; DROP TABLE--") == 'COLDROPTABLE'


class TestValidateTableName:
    """Tests for validate_table_name function."""

    def test_simple_name(self):
        """Should validate simple table name."""
        assert validate_table_name('MY_TABLE') is True

    def test_schema_qualified(self):
        """Should validate schema.table format."""
        assert validate_table_name('DWH.DW_MES_WIP') is True

    def test_invalid_starts_with_number(self):
        """Should reject names starting with number."""
        assert validate_table_name('123TABLE') is False

    def test_invalid_special_chars(self):
        """Should reject names with special characters."""
        assert validate_table_name('TABLE-NAME') is False
        assert validate_table_name('TABLE NAME') is False

    def test_sql_injection_prevention(self):
        """Should reject SQL injection attempts."""
        assert validate_table_name('TABLE; DROP--') is False


class TestErrorLeakageProtection:
    """Tests for exception detail masking in excel-query service."""

    @patch("mes_dashboard.services.excel_query_service.pd.read_excel")
    def test_parse_excel_masks_internal_error_details(self, mock_read_excel):
        mock_read_excel.side_effect = RuntimeError("openpyxl stack trace detail")

        result = parse_excel(MagicMock())

        assert result["error"] == PARSE_ERROR_MESSAGE
        assert "openpyxl" not in result["error"]

    @patch("mes_dashboard.services.excel_query_service.pd.read_excel")
    def test_get_column_unique_values_masks_internal_error_details(self, mock_read_excel):
        mock_read_excel.side_effect = RuntimeError("internal parser detail")

        result = get_column_unique_values(MagicMock(), "LOT_ID")

        assert result["error"] == COLUMN_READ_ERROR_MESSAGE
        assert "internal parser detail" not in result["error"]

    @patch("mes_dashboard.services.excel_query_service.get_db_connection")
    def test_execute_batch_query_masks_internal_error_details(self, mock_get_db):
        mock_cursor = MagicMock()
        mock_cursor.execute.side_effect = RuntimeError("ORA-00942: table missing")
        mock_conn = MagicMock()
        mock_conn.cursor.return_value = mock_cursor
        mock_get_db.return_value = mock_conn

        result = execute_batch_query(
            table_name="DWH.DW_MES_WIP",
            search_column="LOT_ID",
            return_columns=["LOT_ID"],
            search_values=["LOT001"],
        )

        assert result["error"] == QUERY_ERROR_MESSAGE
        assert "ORA-00942" not in result["error"]

    @patch("mes_dashboard.services.excel_query_service.get_db_connection")
    def test_execute_advanced_batch_query_masks_internal_error_details(self, mock_get_db):
        mock_cursor = MagicMock()
        mock_cursor.execute.side_effect = RuntimeError("sensitive sql context")
        mock_conn = MagicMock()
        mock_conn.cursor.return_value = mock_cursor
        mock_get_db.return_value = mock_conn

        result = execute_advanced_batch_query(
            table_name="DWH.DW_MES_WIP",
            search_column="LOT_ID",
            return_columns=["LOT_ID"],
            search_values=["LOT001"],
            query_type="in",
        )

        assert result["error"] == QUERY_ERROR_MESSAGE
        assert "sensitive sql context" not in result["error"]
