"""Gatling rules GAT001-GAT005."""

from __future__ import annotations

import re

from perf_lint.ir.models import Framework, Location, Severity, Violation
from perf_lint.ir.models import ScriptIR
from perf_lint.rules.base import BaseRule, RuleRegistry


@RuleRegistry.register
class GAT001MissingPause(BaseRule):
    rule_id = "GAT001"
    name = "MissingPause"
    description = "Scenario has exec() calls but no pause() calls. Without think time, virtual users hammer the server unrealistically."
    severity = Severity.WARNING
    frameworks = [Framework.GATLING]
    tags = ("think-time", "realism")

    def check(self, ir: ScriptIR) -> list[Violation]:
        exec_count = ir.parsed_data.get("exec_count", 0)
        has_pause = ir.parsed_data.get("has_pause", False)
        if exec_count > 0 and not has_pause:
            return [
                Violation(
                    rule_id=self.rule_id,
                    severity=self.severity,
                    message=f"Found {exec_count} exec() call(s) but no pause() calls. Add think time to simulate realistic user behaviour.",
                    location=Location(line=1),
                    suggestion="Add .pause(1) or .pause(1, 3) between exec() calls to simulate user think time.",
                    fix_example=".exec(http('Homepage').get('/'))\n.pause(1, 3)  // Pause between 1 and 3 seconds\n.exec(http('Search').get('/search?q=test'))",
                )
            ]
        return []


@RuleRegistry.register
class GAT002HardcodedBaseURL(BaseRule):
    rule_id = "GAT002"
    name = "HardcodedBaseURL"
    description = "baseUrl uses a hardcoded IP address. This prevents running the simulation against different environments."
    severity = Severity.ERROR
    frameworks = [Framework.GATLING]
    tags = ("parameterization", "portability")
    tier = "pro"

    def check(self, ir: ScriptIR) -> list[Violation]:
        if ir.parsed_data.get("base_url_is_ip"):
            base_url = ir.parsed_data.get("base_url", "")
            return [
                Violation(
                    rule_id=self.rule_id,
                    severity=self.severity,
                    message=f"baseUrl uses hardcoded IP: {base_url!r}. Use a system property or environment variable instead.",
                    location=Location(line=None),
                    suggestion="Use System.getProperty() or an environment variable to configure the base URL.",
                    fix_example='val baseUrl = System.getProperty("baseUrl", "https://staging.example.com")\n\nval httpProtocol = http.baseUrl(baseUrl)',
                )
            ]
        return []


@RuleRegistry.register
class GAT003MissingAssertions(BaseRule):
    rule_id = "GAT003"
    name = "MissingAssertions"
    description = "No assertions found. Without assertions, the simulation cannot enforce SLOs or detect performance regressions."
    severity = Severity.ERROR
    frameworks = [Framework.GATLING]
    tags = ("assertions", "correctness", "slo")
    fixable = True
    tier = "pro"

    def apply_fix(self, ir: ScriptIR) -> str | None:
        source = ir.raw_content
        if re.search(r'\.assertions?\s*\(', source):
            return None  # Already has assertions

        # Try to find .protocols(...) and append after it
        protocols_re = re.compile(r'(\.protocols\s*\([^)]*\))', re.MULTILINE)
        m = protocols_re.search(source)
        if m:
            assertions_block = (
                "\n    .assertions(\n"
                "      global.responseTime.percentile(95).lt(500),\n"
                "      global.failedRequests.percent.lt(1)\n"
                "    )"
            )
            return source[:m.end()] + assertions_block + source[m.end():]

        # Fall back: append after setUp(...) closing paren
        setup_re = re.compile(r'(setUp\s*\([^)]*\))', re.MULTILINE | re.DOTALL)
        m = setup_re.search(source)
        if m:
            assertions_block = (
                "\n  .assertions(\n"
                "    global.responseTime.percentile(95).lt(500),\n"
                "    global.failedRequests.percent.lt(1)\n"
                "  )"
            )
            return source[:m.end()] + assertions_block + source[m.end():]
        return None

    def check(self, ir: ScriptIR) -> list[Violation]:
        if not ir.parsed_data.get("has_assertions"):
            return [
                Violation(
                    rule_id=self.rule_id,
                    severity=self.severity,
                    message="No assertions found. Add assertions to define pass/fail criteria for the simulation.",
                    location=Location(line=1),
                    suggestion="Add assertions in setUp() to enforce response time and error rate SLOs.",
                    fix_example="setUp(scn.inject(rampUsers(100).during(60)))\n  .protocols(httpProtocol)\n  .assertions(\n    global.responseTime.percentile(95).lt(500),\n    global.failedRequests.percent.lt(1)\n  )",
                )
            ]
        return []


@RuleRegistry.register
class GAT004AggressiveRampup(BaseRule):
    rule_id = "GAT004"
    name = "AggressiveRampup"
    description = "Ramp-up rate exceeds 10 users/second, creating an unrealistic spike load on the system."
    severity = Severity.WARNING
    frameworks = [Framework.GATLING]
    tags = ("rampup", "realism")
    fixable = True
    tier = "team"

    def check(self, ir: ScriptIR) -> list[Violation]:
        violations = []
        for inj in ir.parsed_data.get("injections", []):
            if inj["type"] != "ramp":
                continue
            users = inj["users"]
            duration_secs = inj["duration_secs"]
            if duration_secs > 0:
                rate = users / duration_secs
                if rate > 10:
                    violations.append(
                        Violation(
                            rule_id=self.rule_id,
                            severity=self.severity,
                            message=(
                                f"Ramp-up rate of {rate:.1f} users/second ({users} users in {duration_secs}s) "
                                "exceeds the recommended 10 users/second."
                            ),
                            location=Location(line=None),
                            suggestion=f"Extend ramp duration to at least {users // 10}s for {users} users.",
                            fix_example=f"rampUsers({users}).during({users // 10})",
                        )
                    )
        return violations

    def apply_fix(self, ir: ScriptIR) -> str | None:
        source = ir.raw_content
        ramp_re = re.compile(
            r'(rampUsers\s*\(\s*)(\d+)(\s*\)\s*\.?\s*(?:during|over)\s*\(\s*)(\d+)(\s*)',
            re.MULTILINE,
        )
        changed = False

        def fix_ramp(m: re.Match) -> str:
            nonlocal changed
            users = int(m.group(2))
            duration = int(m.group(4))
            if duration > 0 and users / duration > 10:
                new_duration = max(users // 10, 1)
                changed = True
                return m.group(1) + m.group(2) + m.group(3) + str(new_duration) + m.group(5)
            return m.group(0)

        new_source = ramp_re.sub(fix_ramp, source)
        return new_source if changed else None


@RuleRegistry.register
class GAT005MissingFeeder(BaseRule):
    rule_id = "GAT005"
    name = "MissingFeeder"
    description = "Multiple exec() calls found but no feeder (data source) configured. All virtual users will send identical requests."
    severity = Severity.INFO
    frameworks = [Framework.GATLING]
    tags = ("parameterization", "realism")

    def check(self, ir: ScriptIR) -> list[Violation]:
        exec_count = ir.parsed_data.get("exec_count", 0)
        has_feeder = ir.parsed_data.get("has_feeder", False)
        if exec_count > 1 and not has_feeder:
            return [
                Violation(
                    rule_id=self.rule_id,
                    severity=self.severity,
                    message=f"Found {exec_count} exec() calls but no feeder/data source. Consider parameterising test data.",
                    location=Location(line=1),
                    suggestion="Add a CSV feeder or other data source to vary request parameters across virtual users.",
                    fix_example='val feeder = csv("test-data.csv").random\n\nval scn = scenario("My Scenario")\n  .feed(feeder)\n  .exec(http("Request").get("/api/users/${userId}"))',
                )
            ]
        return []


@RuleRegistry.register
class GAT006MissingMaxDuration(BaseRule):
    rule_id = "GAT006"
    name = "MissingMaxDuration"
    description = (
        "No .maxDuration() configured. Without a max duration, a stalled simulation "
        "(all VUs blocked on slow responses) runs forever, blocking CI pipelines indefinitely."
    )
    severity = Severity.ERROR
    frameworks = [Framework.GATLING]
    tags = ("lifecycle", "ci-integration")
    fixable = True
    tier = "pro"

    def check(self, ir: ScriptIR) -> list[Violation]:
        if ir.parsed_data.get("exec_count", 0) == 0:
            return []
        if not ir.parsed_data.get("has_max_duration"):
            return [Violation(
                rule_id=self.rule_id,
                severity=self.severity,
                message="No .maxDuration() found. Add a maximum duration to prevent stalled simulations from blocking CI pipelines.",
                location=Location(line=1),
                suggestion="Add .maxDuration() to your setUp() call to guarantee the simulation terminates.",
                fix_example="setUp(scn.inject(rampUsers(100).during(60)))\n  .protocols(httpProtocol)\n  .maxDuration(10.minutes)",
            )]
        return []

    def apply_fix(self, ir: ScriptIR) -> str | None:
        source = ir.raw_content
        if re.search(r'\.maxDuration\s*\(', source):
            return None

        # Bracket-counting approach handles arbitrary nesting depth but does not
        # track braces inside Scala string literals or multiline strings.
        # In practice, Gatling .assertions() blocks do not contain string literals
        # with unbalanced parentheses.

        # Find .assertions( block — use bracket counting for correct nesting
        assertions_idx = source.rfind(".assertions(")
        if assertions_idx != -1:
            # Find the matching close paren by bracket counting
            depth = 0
            pos = assertions_idx + len(".assertions(") - 1  # position of '('
            while pos < len(source):
                if source[pos] == '(':
                    depth += 1
                elif source[pos] == ')':
                    depth -= 1
                    if depth == 0:
                        return source[:pos + 1] + "\n    .maxDuration(10.minutes)" + source[pos + 1:]
                pos += 1

        # Fall back to after .protocols(...)
        protocols_re = re.compile(r'(\.protocols\s*\([^)]*\))', re.MULTILINE)
        m = protocols_re.search(source)
        if m:
            return source[:m.end()] + "\n    .maxDuration(10.minutes)" + source[m.end():]

        # Fall back to after setUp(...)
        setup_re = re.compile(r'(setUp\s*\([^)]*\))', re.MULTILINE | re.DOTALL)
        m = setup_re.search(source)
        if m:
            return source[:m.end()] + "\n  .maxDuration(10.minutes)" + source[m.end():]
        return None


@RuleRegistry.register
class GAT007MissingResponseCheck(BaseRule):
    rule_id = "GAT007"
    name = "MissingResponseCheck"
    description = (
        "HTTP request(s) in exec() found without .check(). "
        "Gatling silently passes requests returning 500s, redirects, or empty bodies "
        "unless .check(status.is(200)) is chained — these are tests that cannot fail."
    )
    severity = Severity.ERROR
    frameworks = [Framework.GATLING]
    tags = ("assertions", "correctness")
    tier = "pro"

    def check(self, ir: ScriptIR) -> list[Violation]:
        exec_http = ir.parsed_data.get("exec_http_count", 0)
        with_check = ir.parsed_data.get("exec_with_check_count", 0)
        unchecked = exec_http - with_check
        if exec_http > 0 and unchecked > 0:
            return [Violation(
                rule_id=self.rule_id,
                severity=self.severity,
                message=f"{unchecked} HTTP request(s) in exec() found without .check(). These requests cannot detect application failures.",
                location=Location(line=None),
                suggestion="Chain .check(status.is(200)) onto every HTTP request to detect failures.",
                fix_example='.exec(\n  http("Homepage")\n    .get("/")\n    .check(status.is(200))\n    .check(responseTimeInMillis.lt(1000))\n)',
            )]
        return []


@RuleRegistry.register
class GAT008AtOnceUsersAggressive(BaseRule):
    rule_id = "GAT008"
    name = "AtOnceUsersAggressive"
    description = (
        "atOnceUsers() with more than 10 users detected. atOnceUsers() injects all "
        "VUs simultaneously with zero ramp, equivalent to a DDoS spike. "
        "rampUsers() is almost always the correct choice."
    )
    severity = Severity.WARNING
    frameworks = [Framework.GATLING]
    tags = ("rampup", "realism")
    fixable = True
    tier = "pro"

    def check(self, ir: ScriptIR) -> list[Violation]:
        at_once = ir.parsed_data.get("at_once_users_values", [])
        bad = [n for n in at_once if n > 10]
        if bad:
            return [Violation(
                rule_id=self.rule_id,
                severity=self.severity,
                message=f"atOnceUsers({bad[0]}) injects {bad[0]} VUs simultaneously. Use rampUsers() for gradual load.",
                location=Location(line=None),
                suggestion=f"Replace atOnceUsers({bad[0]}) with rampUsers({bad[0]}).during({bad[0] // 10}) for a safer ramp.",
                fix_example=f"rampUsers({bad[0]}).during({bad[0] // 10})  // Ramp over {bad[0] // 10} seconds",
            )]
        return []

    def apply_fix(self, ir: ScriptIR) -> str | None:
        source = ir.raw_content
        at_once_re = re.compile(r'atOnceUsers\s*\(\s*(\d+)\s*\)')
        changed = False

        def fix_at_once(m: re.Match) -> str:
            nonlocal changed
            n = int(m.group(1))
            if n > 10:
                duration = max(n // 10, 1)
                changed = True
                return f"rampUsers({n}).during({duration})"
            return m.group(0)

        new_source = at_once_re.sub(fix_at_once, source)
        return new_source if changed else None


@RuleRegistry.register
class GAT009MissingConnectionTimeout(BaseRule):
    rule_id = "GAT009"
    name = "MissingConnectionTimeout"
    description = (
        "No .connectionTimeout() or .readTimeout() configured on the HTTP protocol. "
        "Without timeouts, VU threads block indefinitely on slow targets, "
        "eventually exhausting the thread pool."
    )
    severity = Severity.WARNING
    frameworks = [Framework.GATLING]
    tags = ("resilience", "configuration")
    fixable = True
    tier = "pro"

    def check(self, ir: ScriptIR) -> list[Violation]:
        if ir.parsed_data.get("exec_count", 0) == 0:
            return []
        if not ir.parsed_data.get("has_connection_timeout"):
            return [Violation(
                rule_id=self.rule_id,
                severity=self.severity,
                message="No .connectionTimeout() or .readTimeout() configured. Add timeouts to prevent VU thread exhaustion.",
                location=Location(line=None),
                suggestion="Configure timeouts on your HTTP protocol definition.",
                fix_example="val httpProtocol = http\n  .baseUrl(baseUrl)\n  .connectionTimeout(5.seconds)\n  .readTimeout(30.seconds)",
            )]
        return []

    def apply_fix(self, ir: ScriptIR) -> str | None:
        source = ir.raw_content
        if re.search(r'\.(?:connectionTimeout|readTimeout)\s*\(', source):
            return None

        # Find the http protocol block -- look for the line containing .baseUrl(
        base_url_re = re.compile(r'(\.baseUrl\s*\([^)]*\))', re.MULTILINE)
        m = base_url_re.search(source)
        if m:
            timeout_block = "\n    .connectionTimeout(5.seconds)\n    .readTimeout(30.seconds)"
            return source[:m.end()] + timeout_block + source[m.end():]
        return None


@RuleRegistry.register
class GAT010MissingSessionCorrelation(BaseRule):
    rule_id = "GAT010"
    name = "MissingSessionCorrelation"
    description = (
        "Multiple HTTP requests found but no .saveAs() session attribute extraction. "
        "Without correlation, requests after login re-use static recorded values "
        "and will fail under concurrent users (401/403 errors)."
    )
    severity = Severity.WARNING
    frameworks = [Framework.GATLING]
    tags = ("correlation", "correctness", "parameterization")
    tier = "pro"

    def check(self, ir: ScriptIR) -> list[Violation]:
        exec_http = ir.parsed_data.get("exec_http_count", 0)
        if exec_http > 1 and not ir.parsed_data.get("has_save_as"):
            return [Violation(
                rule_id=self.rule_id,
                severity=self.severity,
                message="Multiple HTTP requests found but no .saveAs() correlation. Session tokens from login will not be propagated.",
                location=Location(line=None),
                suggestion="Use .check(jsonPath('$.token').saveAs('authToken')) to extract and reuse session values.",
                fix_example='.exec(\n  http("Login")\n    .post("/login")\n    .body(StringBody("""{"user":"#{user}","pass":"#{pass}"}"""))\n    .check(jsonPath("$.token").saveAs("authToken"))\n)\n.exec(\n  http("API call")\n    .get("/api/data")\n    .header("Authorization", "Bearer #{authToken}")\n)',
            )]
        return []


@RuleRegistry.register
class GAT011AssertionLacksThreshold(BaseRule):
    rule_id = "GAT011"
    name = "AssertionLacksThreshold"
    description = (
        "Assertions present but none reference responseTime or failedRequests. "
        "Assertions without SLO thresholds (e.g. percentile response time, error rate) "
        "do not enforce meaningful performance requirements."
    )
    severity = Severity.INFO
    frameworks = [Framework.GATLING]
    tags = ("assertions", "slo")

    def check(self, ir: ScriptIR) -> list[Violation]:
        if ir.parsed_data.get("has_assertions") and not ir.parsed_data.get("has_response_time_assertion"):
            return [Violation(
                rule_id=self.rule_id,
                severity=self.severity,
                message="Assertions found but none enforce response time or error rate SLOs.",
                location=Location(line=None),
                suggestion="Add responseTime.percentile(95).lt(500) and failedRequests.percent.lt(1) assertions.",
                fix_example=".assertions(\n  global.responseTime.percentile(95).lt(500),\n  global.failedRequests.percent.lt(1)\n)",
            )]
        return []


@RuleRegistry.register
class GAT012HardcodedPauseDuration(BaseRule):
    rule_id = "GAT012"
    name = "HardcodedPauseDuration"
    description = (
        "Only fixed-duration .pause(n) calls found — no .pause(min, max) with variance. "
        "Real users have variable think time. Identical pauses produce unrealistically "
        "synchronised load patterns."
    )
    severity = Severity.INFO
    frameworks = [Framework.GATLING]
    tags = ("think-time", "realism")
    fixable = True
    tier = "team"

    def apply_fix(self, ir: ScriptIR) -> str | None:
        source = ir.raw_content
        # Match .pause(N) where N is a plain integer (not already two-arg)
        # Negative lookahead ensures no comma follows the number inside the parens
        single_pause_re = re.compile(r'(\.pause\s*\(\s*)(\d+)(\s*\))')
        changed = False

        def fix_pause(m: re.Match) -> str:
            nonlocal changed
            n = int(m.group(2))
            if n == 0:
                return m.group(0)  # Don't touch .pause(0) — leave as-is
            changed = True
            return f"{m.group(1)}{n}, {n * 2}{m.group(3)}"

        new_source = single_pause_re.sub(fix_pause, source)
        return new_source if changed else None

    def check(self, ir: ScriptIR) -> list[Violation]:
        if ir.parsed_data.get("has_fixed_pause") and not ir.parsed_data.get("has_uniform_pause"):
            return [Violation(
                rule_id=self.rule_id,
                severity=self.severity,
                message="Only constant .pause(n) found with no variance. Use .pause(min, max) for realistic think time distribution.",
                location=Location(line=None),
                suggestion="Replace .pause(1) with .pause(1, 3) to add variance that mimics real user behaviour.",
                fix_example=".pause(1, 3)  // Uniform pause between 1 and 3 seconds",
            )]
        return []


@RuleRegistry.register
class GAT013MissingHTTP2(BaseRule):
    rule_id = "GAT013"
    name = "MissingHTTP2"
    description = (
        "HTTPS base URL configured but HTTP/2 not enabled. Modern web backends expect "
        "HTTP/2 multiplexing. Without it, Gatling opens a new connection per request, "
        "over-stressing the connection layer rather than the application."
    )
    severity = Severity.INFO
    frameworks = [Framework.GATLING]
    tags = ("http", "realism", "configuration")
    fixable = True

    def apply_fix(self, ir: ScriptIR) -> str | None:
        source = ir.raw_content
        if re.search(r'\.enableHttp2\b', source):
            return None

        # Insert after .baseUrl line
        base_url_re = re.compile(r'(\.baseUrl\s*\([^)]*\))', re.MULTILINE)
        m = base_url_re.search(source)
        if m:
            return source[:m.end()] + "\n    .enableHttp2" + source[m.end():]
        return None

    def check(self, ir: ScriptIR) -> list[Violation]:
        if (
            ir.parsed_data.get("exec_http_count", 0) > 0
            and ir.parsed_data.get("base_url_is_https")
            and not ir.parsed_data.get("has_http2_enabled")
        ):
            return [Violation(
                rule_id=self.rule_id,
                severity=self.severity,
                message="HTTPS endpoint detected but HTTP/2 not enabled. Enable HTTP/2 to match real browser connection behaviour.",
                location=Location(line=None),
                suggestion="Add .enableHttp2 to your HTTP protocol configuration.",
                fix_example="val httpProtocol = http\n  .baseUrl(baseUrl)\n  .enableHttp2",
            )]
        return []
