"""Single test execution."""

from __future__ import annotations

import contextlib
import logging
import time
from dataclasses import dataclass
from typing import Any
from uuid import uuid4

from merit.assertions.base import AssertionResult
from merit.context import (
    ResolverContext,
    TestContext,
    assertions_collector,
    get_runner,
    resolver_context_scope,
    test_context_scope,
)
from merit.resources import ResourceResolver
from merit.resources.resolver import Scope
from merit.testing.execution.interfaces import MeritTest
from merit.testing.execution.result_builder import ResultBuilder
from merit.testing.execution.tracer import TestTracer
from merit.testing.models import MeritTestDefinition, TestExecution, TestResult, TestStatus
from merit.testing.outcomes import FailTest, SkipTest, XFailTest


logger = logging.getLogger(__name__)


@dataclass
class SingleMeritTest(MeritTest):
    """Executes a single test directly."""

    definition: MeritTestDefinition
    params: dict[str, Any]
    tracer: TestTracer
    result_builder: ResultBuilder

    def __post_init__(self) -> None:
        """Validate that this test has no modifiers."""
        if self.definition.modifiers:
            raise ValueError("SingleMeritTest should not have modifiers")

    async def execute(self, resolver: ResourceResolver) -> TestExecution:
        """Execute the test and return result."""
        runner = get_runner()

        if runner is not None and runner.stop_flag:
            return TestExecution(
                definition=self.definition,
                result=TestResult(
                    status=TestStatus.SKIPPED,
                    duration_ms=0,
                    error=Exception("Run stopped early"),
                ),
                execution_id=uuid4(),
            )

        if self.definition.skip_reason:
            return TestExecution(
                definition=self.definition,
                result=TestResult(
                    status=TestStatus.SKIPPED,
                    duration_ms=0,
                    error=Exception(self.definition.skip_reason),
                ),
                execution_id=uuid4(),
            )

        exec_id = uuid4()

        # fork resolver for case isolation
        forked_resolver = resolver.fork_for_case()

        ctx = TestContext(item=self.definition, execution_id=exec_id)
        assertion_results: list[AssertionResult] = []
        error: Exception | None = None

        with self.tracer.span(self.definition) as span:
            start = time.perf_counter()
            imperative_outcome: TestStatus | None = None

            with test_context_scope(ctx), assertions_collector(assertion_results):
                try:
                    semaphore = (
                        runner.semaphore
                        if runner and runner.semaphore
                        else contextlib.nullcontext()
                    )

                    async with semaphore:
                        kwargs = await self._resolve_params(forked_resolver)

                        if runner and runner.stop_flag:
                            imperative_outcome = TestStatus.SKIPPED
                            error = Exception("Run stopped early")
                        else:
                            await self._invoke(kwargs)

                except SkipTest as e:
                    imperative_outcome = TestStatus.SKIPPED
                    error = e
                except FailTest as e:
                    imperative_outcome = TestStatus.FAILED
                    error = e
                except XFailTest as e:
                    imperative_outcome = TestStatus.XFAILED
                    error = e
                except Exception as e:  # noqa: BLE001
                    error = e
                finally:
                    # teardown case scope - must run even if test fails
                    try:
                        await forked_resolver.teardown_scope(Scope.CASE)
                    except Exception as teardown_err:
                        # teardown errors dont mask tests errors
                        logger.warning(f"Error during resource teardown: {teardown_err}")
                        if error is None:
                            error = teardown_err

            duration_ms = (time.perf_counter() - start) * 1000

            if imperative_outcome is not None:
                result = TestResult(
                    status=imperative_outcome,
                    duration_ms=duration_ms,
                    error=error,
                    assertion_results=assertion_results,
                )
            else:
                result = self.result_builder.build(
                    self.definition, duration_ms, assertion_results, error
                )

            trace_id = self.tracer.get_trace_id(span)
            self.tracer.record(span, result)

            return TestExecution(
                definition=self.definition,
                result=result,
                execution_id=exec_id,
                trace_id=trace_id,
            )

    async def _resolve_params(self, resolver: ResourceResolver) -> dict[str, Any]:
        """Resolve test parameters from resources."""
        kwargs = dict(self.params)
        with resolver_context_scope(ResolverContext(consumer_name=self.definition.name)):
            for param in self.definition.params:
                if param not in kwargs:
                    kwargs[param] = await resolver.resolve(param)
        return kwargs

    async def _invoke(self, kwargs: dict[str, Any]) -> None:
        """Invoke the test function."""
        fn = self.definition.fn
        instance = None

        if self.definition.class_name:
            cls = fn.__globals__.get(self.definition.class_name)

            if cls is None:
                raise RuntimeError(
                    f"Test class '{self.definition.class_name}' not found for test '{self.definition.name}'"
                )

            instance = cls()

        if instance:
            if self.definition.is_async:
                await fn(instance, **kwargs)
            else:
                fn(instance, **kwargs)
        elif self.definition.is_async:
            await fn(**kwargs)
        else:
            fn(**kwargs)
