"""Tests for config:set — SDK + CLI."""

from __future__ import annotations

import pytest
from click.testing import CliRunner

from kuberoku.cli.main import cli
from kuberoku.exceptions import (
    AppNotFoundError,
    ConfigLimitError,
    InvalidConfigKeyError,
)
from kuberoku.factory import KuberokuFactory
from kuberoku.sdk.config import ConfigService


class TestConfigSetSDK:
    """SDK-level tests for ConfigService.set()."""

    def test_set_single_var(self, config_service: ConfigService, app_name: str) -> None:
        result = config_service.set(app_name, {"DATABASE_URL": "postgres://localhost/db"})
        assert result["DATABASE_URL"] == "postgres://localhost/db"

    def test_set_multiple_vars(self, config_service: ConfigService, app_name: str) -> None:
        result = config_service.set(app_name, {"FOO": "1", "BAR": "2"})
        assert result["FOO"] == "1"
        assert result["BAR"] == "2"

    def test_set_overwrite_existing(self, config_service: ConfigService, app_name: str) -> None:
        config_service.set(app_name, {"KEY": "old"})
        result = config_service.set(app_name, {"KEY": "new"})
        assert result["KEY"] == "new"

    def test_set_with_secret(self, config_service: ConfigService, app_name: str) -> None:
        result = config_service.set(app_name, {"SECRET_KEY": "s3cret"}, secret=True)
        assert result["SECRET_KEY"] == "s3cret"

    def test_set_preserves_existing_vars(
        self, config_service: ConfigService, app_name: str
    ) -> None:
        config_service.set(app_name, {"A": "1"})
        config_service.set(app_name, {"B": "2"})
        all_vars = config_service.list(app_name)
        assert all_vars["A"] == "1"
        assert all_vars["B"] == "2"

    def test_set_invalid_key_empty(self, config_service: ConfigService, app_name: str) -> None:
        with pytest.raises(InvalidConfigKeyError, match="empty"):
            config_service.set(app_name, {"": "value"})

    def test_set_invalid_key_too_long(self, config_service: ConfigService, app_name: str) -> None:
        with pytest.raises(InvalidConfigKeyError, match="256"):
            config_service.set(app_name, {"A" * 257: "value"})

    def test_set_invalid_key_lowercase(self, config_service: ConfigService, app_name: str) -> None:
        with pytest.raises(InvalidConfigKeyError):
            config_service.set(app_name, {"lowercase": "value"})

    def test_set_invalid_key_special_chars(
        self, config_service: ConfigService, app_name: str
    ) -> None:
        with pytest.raises(InvalidConfigKeyError):
            config_service.set(app_name, {"KEY-WITH-DASH": "value"})

    def test_set_valid_key_with_digits(self, config_service: ConfigService, app_name: str) -> None:
        result = config_service.set(app_name, {"KEY_123": "value"})
        assert result["KEY_123"] == "value"

    def test_set_valid_key_starts_with_underscore(
        self, config_service: ConfigService, app_name: str
    ) -> None:
        result = config_service.set(app_name, {"_KEY": "value"})
        assert result["_KEY"] == "value"

    def test_set_limit_max_vars(self, config_service: ConfigService, app_name: str) -> None:
        # Set 256 vars (the max)
        big_batch = {f"VAR_{i:04d}": f"val{i}" for i in range(256)}
        config_service.set(app_name, big_batch)
        # Adding one more should fail
        with pytest.raises(ConfigLimitError, match="256"):
            config_service.set(app_name, {"ONE_MORE": "boom"})

    def test_set_limit_value_too_large(self, config_service: ConfigService, app_name: str) -> None:
        large_value = "x" * (128 * 1024 + 1)
        with pytest.raises(ConfigLimitError, match="128"):
            config_service.set(app_name, {"BIG": large_value})

    def test_set_app_not_found(self, config_service: ConfigService) -> None:
        with pytest.raises(AppNotFoundError, match="nonexistent"):
            config_service.set("nonexistent", {"KEY": "value"})

    def test_set_on_step_callback(self, config_service: ConfigService, app_name: str) -> None:
        steps: list[str] = []
        config_service.set(app_name, {"KEY": "val"}, on_step=steps.append)
        assert len(steps) >= 1

    def test_set_returns_all_vars_for_source(
        self, config_service: ConfigService, app_name: str
    ) -> None:
        """set() returns all vars in the same source (ConfigMap or Secret)."""
        config_service.set(app_name, {"A": "1"})
        result = config_service.set(app_name, {"B": "2"})
        assert "A" in result
        assert "B" in result

    def test_set_value_with_equals(self, config_service: ConfigService, app_name: str) -> None:
        """Values may contain '=' signs (e.g. connection strings)."""
        config_service.set(app_name, {"DATABASE_URL": "postgres://host/db?opt=val&x=1"})
        result = config_service.list(app_name)
        assert result["DATABASE_URL"] == "postgres://host/db?opt=val&x=1"

    def test_set_empty_value(self, config_service: ConfigService, app_name: str) -> None:
        """Empty string is a valid value."""
        config_service.set(app_name, {"EMPTY": ""})
        result = config_service.list(app_name)
        assert result["EMPTY"] == ""

    def test_set_secret_removes_from_configmap(
        self, config_service: ConfigService, app_name: str
    ) -> None:
        """config:set --secret removes the key from ConfigMap if present."""
        config_service.set(app_name, {"KEY": "plain"})
        config_service.set(app_name, {"KEY": "sensitive"}, secret=True)
        sources = config_service.list_with_sources(app_name)
        assert sources["KEY"] == ("sensitive", "secret")
        # ConfigMap should NOT have the key anymore
        assert all(src != "configmap" for k, (_, src) in sources.items() if k == "KEY")

    def test_set_configmap_removes_from_secret(
        self, config_service: ConfigService, app_name: str
    ) -> None:
        """config:set (no --secret) removes the key from Secret if present."""
        config_service.set(app_name, {"KEY": "sensitive"}, secret=True)
        config_service.set(app_name, {"KEY": "plain"})
        sources = config_service.list_with_sources(app_name)
        assert sources["KEY"] == ("plain", "configmap")
        # Secret should NOT have the key anymore
        assert all(src != "secret" for k, (_, src) in sources.items() if k == "KEY")

    def test_set_cross_source_cleanup_only_affects_set_keys(
        self, config_service: ConfigService, app_name: str
    ) -> None:
        """Cross-source cleanup only removes keys being set, not others."""
        config_service.set(app_name, {"A": "secret_a", "B": "secret_b"}, secret=True)
        # Move A to ConfigMap — B should stay in Secret
        config_service.set(app_name, {"A": "plain_a"})
        sources = config_service.list_with_sources(app_name)
        assert sources["A"] == ("plain_a", "configmap")
        assert sources["B"] == ("secret_b", "secret")

    def test_set_total_size_limit(self, config_service: ConfigService, app_name: str) -> None:
        """Total config size across all vars must not exceed 512 KiB."""
        # Use values just under per-value limit (128 KiB - 1 byte)
        large_value = "x" * (128 * 1024 - 1)
        config_service.set(app_name, {"BIG_A": large_value})
        config_service.set(app_name, {"BIG_B": large_value})
        config_service.set(app_name, {"BIG_C": large_value})
        # 4 * (5 + 131071) = 524,304 > 512 KiB (524,288) — should fail
        with pytest.raises(ConfigLimitError, match="512"):
            config_service.set(app_name, {"BIG_D": large_value})


class TestConfigSetRetry:
    """Tests for optimistic concurrency retry on 409 Conflict."""

    @pytest.fixture(autouse=True)
    def _skip_real_backend(self, request: pytest.FixtureRequest) -> None:
        if request.config.getoption("--backend") == "real":
            pytest.skip("inject_conflict is only available on FakeK8sClient")

    def test_set_configmap_retries_on_conflict(
        self, config_service: ConfigService, app_name: str, factory: KuberokuFactory
    ) -> None:
        """set() retries on 409 and succeeds on 2nd attempt."""
        k8s = factory.k8s
        cm_name = f"{factory.prefix}-env-{app_name}"
        # First update will fail with 409, second succeeds
        k8s.inject_conflict("ConfigMap", factory.namespace, cm_name, count=1)
        result = config_service.set(app_name, {"KEY": "value"})
        assert result["KEY"] == "value"

    def test_set_secret_retries_on_conflict(
        self, config_service: ConfigService, app_name: str, factory: KuberokuFactory
    ) -> None:
        """set(secret=True) retries on 409 and succeeds on 2nd attempt."""
        # Create secret first so inject_conflict has a target
        config_service.set(app_name, {"SEED": "x"}, secret=True)
        k8s = factory.k8s
        secret_name = f"{factory.prefix}-secret-{app_name}"
        k8s.inject_conflict("Secret", factory.namespace, secret_name, count=1)
        result = config_service.set(app_name, {"TOKEN": "s3cret"}, secret=True)
        assert result["TOKEN"] == "s3cret"

    def test_set_configmap_retries_exhausted(
        self, config_service: ConfigService, app_name: str, factory: KuberokuFactory
    ) -> None:
        """set() uses final attempt after retries are exhausted."""
        k8s = factory.k8s
        cm_name = f"{factory.prefix}-env-{app_name}"
        # Fail 3 times (all retries), succeed on final attempt
        k8s.inject_conflict("ConfigMap", factory.namespace, cm_name, count=3)
        result = config_service.set(app_name, {"KEY": "value"})
        assert result["KEY"] == "value"


class TestConfigSetCLI:
    """CLI-level tests for config:set."""

    def test_set_cli(self, cli_runner: CliRunner, factory: KuberokuFactory) -> None:
        factory.apps.create("myapp")
        result = cli_runner.invoke(
            cli, ["config:set", "FOO=bar", "--app", "myapp"], obj={"factory": factory}
        )
        assert result.exit_code == 0
        assert "FOO" in result.output

    def test_set_cli_multiple(self, cli_runner: CliRunner, factory: KuberokuFactory) -> None:
        factory.apps.create("myapp")
        result = cli_runner.invoke(
            cli,
            ["config:set", "A=1", "B=2", "--app", "myapp"],
            obj={"factory": factory},
        )
        assert result.exit_code == 0
        assert "A" in result.output
        assert "B" in result.output

    def test_set_cli_secret(self, cli_runner: CliRunner, factory: KuberokuFactory) -> None:
        factory.apps.create("myapp")
        result = cli_runner.invoke(
            cli,
            ["config:set", "TOKEN=abc", "--secret", "--app", "myapp"],
            obj={"factory": factory},
        )
        assert result.exit_code == 0
        assert "TOKEN" in result.output
        assert "abc" not in result.output  # secret value must be masked
        assert "********" in result.output

    def test_set_cli_invalid_format(self, cli_runner: CliRunner, factory: KuberokuFactory) -> None:
        factory.apps.create("myapp")
        result = cli_runner.invoke(
            cli,
            ["config:set", "INVALID", "--app", "myapp"],
            obj={"factory": factory},
        )
        assert result.exit_code != 0

    def test_set_cli_space_syntax(self, cli_runner: CliRunner, factory: KuberokuFactory) -> None:
        factory.apps.create("myapp")
        result = cli_runner.invoke(
            cli,
            ["config", "set", "X=1", "--app", "myapp"],
            obj={"factory": factory},
        )
        assert result.exit_code == 0

    def test_set_cli_no_args(self, cli_runner: CliRunner, factory: KuberokuFactory) -> None:
        factory.apps.create("myapp")
        result = cli_runner.invoke(cli, ["config:set", "--app", "myapp"], obj={"factory": factory})
        assert result.exit_code != 0

    def test_set_cli_value_with_equals(
        self, cli_runner: CliRunner, factory: KuberokuFactory
    ) -> None:
        """CLI correctly splits KEY=VALUE on first '=' only."""
        factory.apps.create("myapp")
        result = cli_runner.invoke(
            cli,
            ["config:set", "URL=postgres://h/db?opt=val", "--app", "myapp"],
            obj={"factory": factory},
        )
        assert result.exit_code == 0
        # Verify the value was stored correctly
        val, _ = factory.config.get("myapp", "URL")
        assert val == "postgres://h/db?opt=val"

    def test_set_cli_empty_value(self, cli_runner: CliRunner, factory: KuberokuFactory) -> None:
        """CLI allows KEY= (empty value)."""
        factory.apps.create("myapp")
        result = cli_runner.invoke(
            cli,
            ["config:set", "EMPTY=", "--app", "myapp"],
            obj={"factory": factory},
        )
        assert result.exit_code == 0
        val, _ = factory.config.get("myapp", "EMPTY")
        assert val == ""
