"""Tests for neonlink.config."""

import os
import pytest

from neonlink.config import Config, new_config_from_lookup, new_config_from_map


class TestDefaultConfig:
    def test_defaults(self):
        cfg = Config()
        assert cfg.brokers == ["localhost:9092"]
        assert cfg.max_poll_records == 10
        assert cfg.session_timeout_sec == 30
        assert cfg.auto_offset_reset == "earliest"
        assert cfg.required_acks == "all"
        assert cfg.transactional_id == ""
        assert cfg.cb_max_failures == 5
        assert cfg.retry_max_attempts == 3
        assert cfg.retry_initial_wait_ms == 500
        assert cfg.retry_max_wait_ms == 30000
        assert cfg.dlq_topic == "errors-dlq"
        assert cfg.poison_pill_threshold == 5


class TestNewConfigFromLookup:
    def test_loads_from_lookup(self):
        values = {
            "NEONLINK_BROKERS": "broker1:9092,broker2:9092",
            "NEONLINK_TLS_ENABLED": "true",
            "NEONLINK_CONSUMER_GROUP": "test-group",
            "NEONLINK_MAX_POLL_RECORDS": "50",
            "NEONLINK_SESSION_TIMEOUT_SEC": "45",
            "NEONLINK_AUTO_OFFSET_RESET": "latest",
            "NEONLINK_REQUIRED_ACKS": "all",
            "NEONLINK_TRANSACTIONAL_ID": "txn-test-0",
            "NEONLINK_CB_MAX_FAILURES": "10",
            "NEONLINK_RETRY_MAX_ATTEMPTS": "5",
            "NEONLINK_RETRY_INITIAL_WAIT_MS": "1000",
            "NEONLINK_RETRY_MAX_WAIT_MS": "60000",
            "NEONLINK_DLQ_TOPIC": "custom-dlq",
            "NEONLINK_POISON_THRESHOLD": "3",
            "NEONLINK_CLIENT_ID": "test-client",
        }
        cfg = new_config_from_lookup(lambda k: (values.get(k, ""), k in values))

        assert cfg.brokers == ["broker1:9092", "broker2:9092"]
        assert cfg.tls_enabled is True
        assert cfg.consumer_group == "test-group"
        assert cfg.max_poll_records == 50
        assert cfg.session_timeout_sec == 45
        assert cfg.auto_offset_reset == "latest"
        assert cfg.transactional_id == "txn-test-0"
        assert cfg.cb_max_failures == 10
        assert cfg.retry_max_attempts == 5
        assert cfg.retry_initial_wait_ms == 1000
        assert cfg.retry_max_wait_ms == 60000
        assert cfg.dlq_topic == "custom-dlq"
        assert cfg.poison_pill_threshold == 3
        assert cfg.client_id == "test-client"

    def test_env_backed_parity(self, monkeypatch):
        monkeypatch.setenv("NEONLINK_BROKERS", "env-broker:9092")
        monkeypatch.setenv("NEONLINK_CONSUMER_GROUP", "env-group")

        cfg = new_config_from_lookup(
            lambda k: (os.environ.get(k, ""), k in os.environ)
        )
        assert cfg.brokers == ["env-broker:9092"]
        assert cfg.consumer_group == "env-group"


class TestNewConfigFromMap:
    def test_map_constructor(self):
        cfg = new_config_from_map({
            "NEONLINK_BROKERS": "b1:9092",
            "NEONLINK_CONSUMER_GROUP": "map-group",
        })
        assert cfg.brokers == ["b1:9092"]
        assert cfg.consumer_group == "map-group"

    def test_absent_keys_retain_defaults(self):
        cfg = new_config_from_map({})
        assert cfg.brokers == ["localhost:9092"]
        assert cfg.max_poll_records == 10
        assert cfg.dlq_topic == "errors-dlq"
        assert cfg.required_acks == "all"

    def test_new_fields(self):
        cfg = new_config_from_map({
            "NEONLINK_LINGER_MS": "10",
            "NEONLINK_BATCH_SIZE": "32768",
            "NEONLINK_MAX_MESSAGE_BYTES": "2097152",
            "NEONLINK_MAX_BUFFERED_RECORDS": "500",
        })
        assert cfg.linger_ms == 10
        assert cfg.batch_size == 32768
        assert cfg.max_message_bytes == 2097152
        assert cfg.max_buffered_records == 500


class TestLookupParsingBehaviors:
    def test_invalid_bool_falls_back(self):
        cfg = new_config_from_map({"NEONLINK_TLS_ENABLED": "invalid"})
        assert cfg.tls_enabled is False

    def test_invalid_int_falls_back(self):
        cfg = new_config_from_map({"NEONLINK_MAX_POLL_RECORDS": "not-a-number"})
        assert cfg.max_poll_records == 10

    def test_empty_string_treated_as_absent(self):
        cfg = new_config_from_map({"NEONLINK_CLIENT_ID": ""})
        assert cfg.client_id == ""

class TestConfigValidate:
    def test_valid_default(self):
        Config().validate()

    def test_no_brokers(self):
        with pytest.raises(ValueError, match="at least one broker"):
            Config(brokers=[]).validate()

    def test_empty_broker(self):
        with pytest.raises(ValueError, match="cannot be empty"):
            Config(brokers=[""]).validate()

    def test_tls_without_certs(self):
        with pytest.raises(ValueError, match="TLS enabled"):
            Config(tls_enabled=True).validate()

    def test_sasl_without_credentials(self):
        with pytest.raises(ValueError, match="username/password"):
            Config(sasl_mechanism="PLAIN").validate()

    def test_unsupported_sasl(self):
        with pytest.raises(ValueError, match="unsupported SASL"):
            Config(
                sasl_mechanism="KERBEROS",
                sasl_username="u",
                sasl_password="p",
            ).validate()

    def test_valid_sasl_plain(self):
        Config(
            sasl_mechanism="PLAIN",
            sasl_username="u",
            sasl_password="p",
        ).validate()

    def test_valid_sasl_scram256(self):
        Config(
            sasl_mechanism="SCRAM-SHA-256",
            sasl_username="u",
            sasl_password="p",
        ).validate()

    def test_poison_threshold_zero(self):
        with pytest.raises(ValueError, match="poison pill threshold"):
            Config(poison_pill_threshold=0).validate()


class TestConfluentConfig:
    def test_basic(self):
        cfg = Config(brokers=["b1:9092", "b2:9092"], client_id="test")
        conf = cfg.to_confluent_config()
        assert conf["bootstrap.servers"] == "b1:9092,b2:9092"
        assert conf["client.id"] == "test"

    def test_tls(self):
        cfg = Config(
            tls_enabled=True,
            tls_ca_file="/ca.pem",
            tls_cert_file="/cert.pem",
            tls_key_file="/key.pem",
        )
        conf = cfg.to_confluent_config()
        assert conf["security.protocol"] == "SSL"
        assert conf["ssl.ca.location"] == "/ca.pem"

    def test_sasl_plain(self):
        cfg = Config(
            sasl_mechanism="PLAIN",
            sasl_username="user",
            sasl_password="pass",
        )
        conf = cfg.to_confluent_config()
        assert conf["security.protocol"] == "SASL_PLAINTEXT"
        assert conf["sasl.mechanism"] == "PLAIN"
        assert conf["sasl.username"] == "user"

    def test_sasl_with_tls(self):
        cfg = Config(
            tls_enabled=True,
            tls_ca_file="/ca.pem",
            sasl_mechanism="SCRAM-SHA-512",
            sasl_username="user",
            sasl_password="pass",
        )
        conf = cfg.to_confluent_config()
        assert conf["security.protocol"] == "SASL_SSL"

    def test_producer_config(self):
        cfg = Config()
        conf = cfg.to_producer_config()
        assert conf["acks"] == "all"
        assert conf["retries"] == "3"
        assert conf["retry.backoff.ms"] == "500"
        assert conf["retry.backoff.max.ms"] == "30000"

    def test_producer_config_custom_retry(self):
        cfg = Config(retry_initial_wait_ms=1000, retry_max_wait_ms=60000)
        conf = cfg.to_producer_config()
        assert conf["retry.backoff.ms"] == "1000"
        assert conf["retry.backoff.max.ms"] == "60000"

    def test_consumer_config(self):
        cfg = Config(consumer_group="test-group")
        conf = cfg.to_consumer_config()
        assert conf["group.id"] == "test-group"
        assert conf["enable.auto.commit"] == "false"
        assert conf["auto.offset.reset"] == "earliest"

    def test_transactional_config(self):
        cfg = Config(transactional_id="test-txn-0")
        conf = cfg.to_transactional_config()
        assert conf["acks"] == "all"
        assert conf["transactional.id"] == "test-txn-0"
        assert conf["enable.idempotence"] == "true"

    def test_consumer_config_cooperative_sticky(self):
        cfg = Config(consumer_group="test-group")
        conf = cfg.to_consumer_config()
        assert conf["partition.assignment.strategy"] == "cooperative-sticky"

    def test_producer_config_linger_batch(self):
        cfg = Config(linger_ms=10, batch_size=32768, max_message_bytes=2097152)
        conf = cfg.to_producer_config()
        assert conf["linger.ms"] == "10"
        assert conf["batch.size"] == "32768"
        assert conf["message.max.bytes"] == "2097152"

    def test_transactional_config_requires_id(self):
        cfg = Config()
        with pytest.raises(ValueError, match="transactional_id is required"):
            cfg.to_transactional_config()


class TestConfigNewEnvVars:
    def test_defaults(self):
        cfg = Config()
        assert cfg.linger_ms == 5
        assert cfg.batch_size == 16384
        assert cfg.max_message_bytes == 1048576
        assert cfg.max_buffered_records == 500


class TestPayloadValidation:
    def test_payload_too_large(self):
        from neonlink.errors import PayloadTooLargeError

        with pytest.raises(PayloadTooLargeError):
            raise PayloadTooLargeError("too big")
