import sys
from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal, TypedDict, TypeVar

if sys.version_info >= (3, 11):
    from typing import NotRequired
else:
    from typing_extensions import NotRequired

if TYPE_CHECKING:
    import httpx
    import requests  # type: ignore[import-untyped]
    from django.http import HttpRequest as DjangoHttpRequest
    from django.http import HttpResponse as DjangoHttpResponse
    from django.test import AsyncClient as DjangoAsyncClient
    from django.test import Client as DjangoClient
    from flask.testing import FlaskClient
    from werkzeug.test import Client as WerkzeugClient
    from werkzeug.wrappers import Request as WerkzeugRequest
    from werkzeug.wrappers import Response as WerkzeugResponse

# Return type for CoverageMap.statistic()
class OperationStatistic(TypedDict):
    seen: int
    total: int
    timeline: list[tuple[float, int]]

class ParameterStatistic(TypedDict):
    positive: int
    negative: int
    partial: int
    full: int
    total: int
    timeline: list[tuple[float, int]]

class KeywordStatistic(TypedDict):
    positive: int
    negative: int
    partial: int
    full: int
    total: int
    timeline: list[tuple[float, int]]

class ExampleStatistic(TypedDict):
    seen: int
    total: int
    timeline: list[tuple[float, int]]

class ResponseStatistic(TypedDict):
    seen: int
    total: int
    elapsed_total: float
    timeline: list[tuple[float, int]]

class CoverageStatistic(TypedDict):
    operations: OperationStatistic
    parameters: ParameterStatistic
    keywords: KeywordStatistic
    examples: ExampleStatistic
    responses: ResponseStatistic

class BinaryCoverage(TypedDict):
    covered: int
    total: int
    percent: float

class TernaryCoverage(TypedDict):
    covered: int
    partial: int
    total: int
    percent: float

class KeywordReport(TypedDict):
    schema_path: str
    state: Literal["full", "partial", "miss", "unsupported"]
    valid: NotRequired[int]
    invalid: NotRequired[int]
    reason: NotRequired[str]

class NamedParameterReport(TypedDict):
    location: str
    name: str
    hits: int
    coverage: TernaryCoverage
    keywords: NotRequired[list[KeywordReport]]

class BodyParameterReport(TypedDict):
    location: str
    media_type: str
    hits: int
    coverage: TernaryCoverage
    keywords: NotRequired[list[KeywordReport]]

ParameterReport = NamedParameterReport | BodyParameterReport

class OperationCoverage(TypedDict):
    parameters: TernaryCoverage
    keywords: TernaryCoverage
    examples: BinaryCoverage
    responses: BinaryCoverage

class ResponseReport(TypedDict):
    status: str
    hits: int
    avg_elapsed_ms: NotRequired[float]

class ShadowedWarning(TypedDict):
    type: Literal["shadowed"]
    by: str

OperationWarning = ShadowedWarning

class OperationReport(TypedDict):
    method: str
    path: str
    operation_id: NotRequired[str]
    hits: int
    warnings: NotRequired[list[OperationWarning]]
    coverage: OperationCoverage
    parameters: NotRequired[list[ParameterReport]]
    responses: NotRequired[list[ResponseReport]]
    unexpected_responses: NotRequired[list[ResponseReport]]
    errors: list[str]

class ReportSummary(TypedDict):
    operations: BinaryCoverage
    parameters: TernaryCoverage
    keywords: TernaryCoverage
    examples: BinaryCoverage
    responses: BinaryCoverage

class JsonReportData(TypedDict):
    # "$schema" key is present in the dict; access it as data["$schema"]
    version: str
    schema_location: NotRequired[str]
    generated_at: str
    summary: ReportSummary
    operations: list[OperationReport]

class UncoveredKeyword(TypedDict):
    method: str
    path: str
    location: str
    parameter: str
    schema_path: str
    state: str
    satisfiability: str

# Return type for CoverageMap.coverage_gaps()
class OperationUnseenGap(TypedDict):
    method: str
    path: str
    kind: Literal["operation_unseen"]

class ResponseUncoveredGap(TypedDict):
    method: str
    path: str
    kind: Literal["response_uncovered"]
    status_code: str

class ParameterUncoveredGap(TypedDict):
    method: str
    path: str
    kind: Literal["parameter_uncovered"]
    location: str
    parameter: str

class KeywordGap(TypedDict):
    method: str
    path: str
    kind: Literal["keyword_miss", "keyword_needs_valid", "keyword_needs_invalid"]
    location: str
    parameter: str
    schema_path: str
    satisfiability: str

class ExampleUnseenGap(TypedDict):
    method: str
    path: str
    kind: Literal["example_unseen"]
    location: str
    parameter: str

class ThresholdViolation(TypedDict):
    dimension: Literal["operations", "parameters", "keywords", "examples", "responses"]
    actual: float
    minimum: float

CoverageGap = OperationUnseenGap | ResponseUncoveredGap | ParameterUncoveredGap | KeywordGap | ExampleUnseenGap

_T = TypeVar("_T")
_SelfCoverageMap = TypeVar("_SelfCoverageMap", bound="CoverageMap")

class CoverageEvent:
    """A parsed coverage event label. Frozen slotted dataclass at runtime."""

    namespace: str
    domain: str
    method: str
    path: str
    location: str
    parameter: str
    schema_path: str
    def __init__(
        self,
        namespace: str,
        domain: str,
        method: str,
        path: str,
        location: str,
        parameter: str,
        schema_path: str,
    ) -> None: ...
    def __hash__(self) -> int: ...
    def __eq__(self, other: object) -> bool: ...

def parse_event_label(label: str) -> CoverageEvent: ...

class HttpRequest:
    method: str
    url: str
    body: bytes | None
    headers: dict[str, str]

    def __init__(
        self, method: str, url: str, body: bytes | None = None, headers: dict[str, str] | None = None
    ) -> None: ...

class HttpResponse:
    status_code: int
    elapsed: float

    def __init__(self, status_code: int, elapsed: float) -> None: ...

class HttpInteraction:
    request: HttpRequest
    response: HttpResponse | None
    timestamp: float

    def __init__(self, request: HttpRequest, response: HttpResponse | None = None, timestamp: float = ...) -> None: ...

class CoverageMap:
    @classmethod
    def from_dict(
        cls: type[_SelfCoverageMap],
        schema: dict[str, Any],
        *,
        base_path: str | None = None,
        location: str | None = None,
    ) -> _SelfCoverageMap: ...
    @classmethod
    def from_path(
        cls: type[_SelfCoverageMap], path: str | Path, *, base_path: str | None = None
    ) -> _SelfCoverageMap: ...
    @classmethod
    def from_url(cls: type[_SelfCoverageMap], url: str, *, base_path: str | None = None) -> _SelfCoverageMap: ...
    @property
    def requests(self) -> RequestsPlugin: ...
    @property
    def httpx(self) -> HttpxPlugin: ...
    @property
    def postman(self) -> PostmanPlugin: ...
    @property
    def har(self) -> HarPlugin: ...
    @property
    def vcr(self) -> VcrPlugin: ...
    def record_error(self, method: str, path: str, message: str) -> None: ...
    def record_schemathesis_interactions(
        self, method: str, path: str, interactions: list[HttpInteraction]
    ) -> list[tuple[str, str]]: ...
    def record(
        self, request: HttpRequest, response: HttpResponse | None = None, timestamp: float | int | None = None
    ) -> None: ...
    def statistic(self) -> CoverageStatistic: ...
    def uncovered_keywords(self) -> list[UncoveredKeyword]: ...  # deprecated: use coverage_gaps()
    def coverage_gaps(self) -> list[CoverageGap]: ...
    def check_thresholds(
        self,
        *,
        operations: float | None = None,
        parameters: float | None = None,
        keywords: float | None = None,
        examples: float | None = None,
        responses: float | None = None,
    ) -> list[ThresholdViolation]: ...
    def build_json_report(self) -> JsonReportData: ...
    def operation_report(self, method: str, path: str) -> OperationReport | None: ...
    def operation_reports(self) -> list[OperationReport]: ...
    def generate_markdown_report(
        self,
        *,
        report_url: str | None = None,
        weak_threshold: float | None = None,
        is_pull_request: bool = False,
        sub_text: str | None = None,
    ) -> str: ...
    @property
    def django(self) -> DjangoPlugin: ...
    @property
    def flask(self) -> FlaskPlugin: ...
    def generate_text_report(
        self,
        *,
        width: int,
        colored: bool = True,
        skip_covered: bool = False,
        skip_empty: bool = False,
        show_missing: list[str] | None = None,
    ) -> str: ...
    def generate_report(
        self, *, format: Literal["html", "json", "markdown"] = "html", title: str | None = None
    ) -> str: ...
    def save_report(
        self,
        *,
        output_file: str | None = None,
        format: Literal["html", "json", "markdown"] = "html",
        title: str | None = None,
        show_suggestions: bool = False,
    ) -> None: ...
    def drain_interactions(self) -> list[dict[str, Any]]: ...
    def get_schema(self) -> dict[str, Any] | None: ...

class ResponseHook:
    def __init__(self, coverage: CoverageMap) -> None: ...
    def __call__(self, response: requests.Response, timestamp: float | int | None = None, **kwargs: Any) -> None: ...

class SyncResponseHook:
    def __init__(self, coverage: CoverageMap) -> None: ...
    def __call__(self, response: httpx.Response) -> None: ...

class AsyncResponseHookAwaitable:
    def __init__(self, coverage: CoverageMap, request: Any, response: Any, result: Any) -> None: ...
    def __await__(self) -> Any: ...

class AsyncResponseHook:
    def __init__(self, coverage: CoverageMap) -> None: ...
    def __call__(self, response: httpx.Response) -> AsyncResponseHookAwaitable: ...

class RequestsPlugin:
    def __init__(self, coverage: CoverageMap) -> None: ...
    def track_session(self, session: requests.Session) -> requests.Session: ...
    def record(
        self,
        request: requests.Request,
        response: requests.Response | None = None,
        timestamp: float | int | None = None,
    ) -> None: ...
    def response_hook(self) -> ResponseHook: ...

class HttpxPlugin:
    def __init__(self, coverage: CoverageMap) -> None: ...
    def track_client(self, client: httpx.Client | httpx.AsyncClient) -> httpx.Client | httpx.AsyncClient: ...
    def record(
        self,
        request: httpx.Request,
        response: httpx.Response | None = None,
        timestamp: float | int | None = None,
    ) -> None: ...
    def response_hook(self) -> SyncResponseHook: ...
    def async_response_hook(self) -> AsyncResponseHook: ...

class PostmanPlugin:
    def __init__(self, coverage: CoverageMap) -> None: ...
    def record_from_path(self, path: str | Path) -> None: ...
    def record_from_dict(self, data: dict[str, Any]) -> None: ...

class HarPlugin:
    def __init__(self, coverage: CoverageMap) -> None: ...
    def record_from_path(self, path: str | Path) -> None: ...
    def record_from_dict(self, archive: dict[str, Any]) -> None: ...

class VcrPlugin:
    def __init__(self, coverage: CoverageMap) -> None: ...
    def record_from_path(self, path: str | Path) -> None: ...
    def record_from_dict(self, cassette: dict[str, Any]) -> None: ...

class DjangoPlugin:
    def __init__(self, coverage: CoverageMap) -> None: ...
    def track_client(self, client: DjangoClient | DjangoAsyncClient) -> DjangoClient | DjangoAsyncClient: ...
    def record(
        self,
        request: DjangoHttpRequest,
        response: DjangoHttpResponse | None = None,
        timestamp: float | int | None = None,
        elapsed: float | None = None,
    ) -> None: ...

class FlaskPlugin:
    def __init__(self, coverage: CoverageMap) -> None: ...
    def track_client(self, client: WerkzeugClient | FlaskClient) -> WerkzeugClient | FlaskClient: ...
    def record(
        self,
        request: WerkzeugRequest,
        response: WerkzeugResponse | None = None,
        timestamp: float | int | None = None,
        elapsed: float | None = None,
    ) -> None: ...
