"""Unit tests for bind_sql_params."""

import unittest

from ecmind_blue_client.ecm._ecm_sql_params import bind_sql_params


class TestBindSqlParamsPercent(unittest.TestCase):
    """Tests for the %% literal escape."""

    def test_literal_percent(self):
        """Double %% is replaced with a single literal percent sign."""
        self.assertEqual(bind_sql_params("100%%"), "100%")

    def test_literal_percent_does_not_consume_param(self):
        """Double %% does not consume a positional parameter; extra params raise ValueError."""
        with self.assertRaises(ValueError):
            bind_sql_params("100%%", "extra")


class TestBindSqlParamsString(unittest.TestCase):
    """Tests for the %s placeholder."""

    def test_simple_string(self):
        """A plain string value is wrapped in single quotes."""
        self.assertEqual(bind_sql_params("name = %s", "Alice"), "name = 'Alice'")

    def test_string_with_single_quote(self):
        """Single quotes inside the value are escaped by doubling."""
        self.assertEqual(bind_sql_params("name = %s", "O'Brien"), "name = 'O''Brien'")

    def test_string_with_whitespace(self):
        """Strings containing spaces are quoted as-is."""
        self.assertEqual(bind_sql_params("name = %s", "Alice Smith"), "name = 'Alice Smith'")

    def test_string_injection_line_comment(self):
        """A line-comment injection attempt is safely quoted."""
        result = bind_sql_params("WHERE user = %s AND pw = 'x'", "admin'--")
        self.assertEqual(result, "WHERE user = 'admin''--' AND pw = 'x'")

    def test_string_injection_union(self):
        """A UNION-based injection attempt is safely quoted."""
        result = bind_sql_params("WHERE user = %s", "' OR '1'='1")
        self.assertEqual(result, "WHERE user = ''' OR ''1''=''1'")

    def test_string_injection_stacked_query(self):
        """A stacked-query injection attempt is safely quoted."""
        result = bind_sql_params("WHERE user = %s", "x'; DROP TABLE benutzer; --")
        self.assertEqual(result, "WHERE user = 'x''; DROP TABLE benutzer; --'")


class TestBindSqlParamsWord(unittest.TestCase):
    """Tests for the %w placeholder."""

    def test_simple_word(self):
        """A single-word value is wrapped in single quotes."""
        self.assertEqual(bind_sql_params("status = %w", "active"), "status = 'active'")

    def test_word_with_single_quote(self):
        """Existing single quotes in the word value are doubled."""
        self.assertEqual(bind_sql_params("val = %w", "it''s"), "val = 'it''''s'")

    def test_word_raises_on_space(self):
        """A value with a space is rejected by %w."""
        with self.assertRaises(ValueError):
            bind_sql_params("val = %w", "two words")

    def test_word_raises_on_tab(self):
        """A value with a tab character is rejected by %w."""
        with self.assertRaises(ValueError):
            bind_sql_params("val = %w", "tab\there")

    def test_word_raises_on_newline(self):
        """A value with a newline character is rejected by %w."""
        with self.assertRaises(ValueError):
            bind_sql_params("val = %w", "new\nline")

    def test_word_raises_on_line_comment(self):
        """A value containing -- is rejected by %w."""
        with self.assertRaises(ValueError):
            bind_sql_params("val = %w", "x--injection")

    def test_word_raises_on_block_comment_open(self):
        """A value containing /* is rejected by %w."""
        with self.assertRaises(ValueError):
            bind_sql_params("val = %w", "x/*injection")

    def test_word_raises_on_block_comment_close(self):
        """A value containing */ is rejected by %w."""
        with self.assertRaises(ValueError):
            bind_sql_params("val = %w", "x*/injection")


class TestBindSqlParamsInteger(unittest.TestCase):
    """Tests for the %d placeholder."""

    def test_integer(self):
        """An integer value is rendered without quotes."""
        self.assertEqual(bind_sql_params("id = %d", 42), "id = 42")

    def test_integer_from_string(self):
        """A string that represents a valid integer is converted."""
        self.assertEqual(bind_sql_params("id = %d", "7"), "id = 7")

    def test_integer_raises_on_float_string(self):
        """A float string like '3.14' is rejected by %d."""
        with self.assertRaises(ValueError):
            bind_sql_params("id = %d", "3.14")

    def test_integer_raises_on_text(self):
        """A non-numeric string is rejected by %d."""
        with self.assertRaises(ValueError):
            bind_sql_params("id = %d", "not_a_number")

    def test_integer_injection_not_possible(self):
        """A SQL injection attempt via %d is rejected because it is not a valid integer."""
        with self.assertRaises(ValueError):
            bind_sql_params("id = %d", "1; DROP TABLE benutzer")


class TestBindSqlParamsFloat(unittest.TestCase):
    """Tests for the %f placeholder."""

    def test_float(self):
        """A float value is rendered as a decimal number without quotes."""
        self.assertEqual(bind_sql_params("val = %f", 3.14), "val = 3.14")

    def test_float_from_int(self):
        """An integer passed to %f is rendered with a decimal point."""
        self.assertEqual(bind_sql_params("val = %f", 2), "val = 2.0")

    def test_float_from_string(self):
        """A string representing a float is converted and rendered."""
        self.assertEqual(bind_sql_params("val = %f", "1.5"), "val = 1.5")

    def test_float_raises_on_text(self):
        """A non-numeric string is rejected by %f."""
        with self.assertRaises(ValueError):
            bind_sql_params("val = %f", "not_a_float")


class TestBindSqlParamsUnquoted(unittest.TestCase):
    """Tests for the %u placeholder (unquoted identifier)."""

    def test_simple_identifier(self):
        """A simple alphanumeric identifier is inserted without quotes."""
        self.assertEqual(bind_sql_params("FROM %u", "benutzer"), "FROM benutzer")

    def test_identifier_with_underscore(self):
        """An identifier containing underscores is accepted."""
        self.assertEqual(bind_sql_params("FROM %u", "my_table"), "FROM my_table")

    def test_identifier_with_allowed_special_chars(self):
        """Identifiers with allowed special characters (@, $, #) are accepted."""
        self.assertEqual(bind_sql_params("col = %u", "col@$#"), "col = col@$#")

    def test_identifier_raises_on_space(self):
        """An identifier containing a space is rejected by %u."""
        with self.assertRaises(ValueError):
            bind_sql_params("FROM %u", "my table")

    def test_identifier_raises_on_single_quote(self):
        """An identifier containing a single quote is rejected by %u."""
        with self.assertRaises(ValueError):
            bind_sql_params("FROM %u", "table'name")

    def test_identifier_raises_on_semicolon(self):
        """An identifier containing a semicolon is rejected by %u."""
        with self.assertRaises(ValueError):
            bind_sql_params("FROM %u", "t; DROP TABLE t")

    def test_identifier_raises_on_empty(self):
        """An empty string is rejected by %u."""
        with self.assertRaises(ValueError):
            bind_sql_params("FROM %u", "")


class TestBindSqlParamsMixed(unittest.TestCase):
    """Tests combining multiple placeholder types."""

    def test_mixed_placeholders(self):
        """Multiple placeholder types in one template are all substituted correctly."""
        result = bind_sql_params(
            "SELECT * FROM %u WHERE name = %s AND age > %d AND score > %f",
            "benutzer", "O'Brien", 18, 4.5,
        )
        self.assertEqual(
            result,
            "SELECT * FROM benutzer WHERE name = 'O''Brien' AND age > 18 AND score > 4.5",
        )

    def test_no_placeholders_no_params(self):
        """A template without any placeholders is returned unchanged."""
        sql = "SELECT 1"
        self.assertEqual(bind_sql_params(sql), sql)

    def test_too_few_params_raises(self):
        """Fewer params than placeholders raises ValueError."""
        with self.assertRaises(ValueError):
            bind_sql_params("WHERE a = %s AND b = %s", "only_one")

    def test_too_many_params_raises(self):
        """More params than placeholders raises ValueError."""
        with self.assertRaises(ValueError):
            bind_sql_params("WHERE a = %s", "one", "two")

    def test_unknown_placeholder_raises(self):
        """An unknown placeholder type raises ValueError."""
        with self.assertRaises(ValueError):
            bind_sql_params("WHERE a = %x", "value")
