import related
import jmespath
import os

from . import Case, Namespace, Step, Suite, Validator, Timer, Session
from . import enums, get_logger, const, log_with_success, utils

# https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#2xx_Success
HTTP_SUCCESS = [200, 201, 202, 203, 204, 205, 206, 207, 208, 226]


@related.immutable
class ValidationResult(object):
    actual = related.ChildField(object)
    expect = related.ChildField(object)
    success = related.BooleanField()
    validator = related.ChildField(Validator)


@related.immutable
class Fetch(object):
    url = related.StringField()
    method = related.StringField()
    kwargs = related.ChildField(dict)
    is_form = related.BooleanField()

    def get_kwargs(self, is_aiohttp):
        kw = self.kwargs.copy()
        data = kw.get("data", None)

        # aiohttp is different from requests in handling files
        # http://aiohttp.readthedocs.io/en/stable/client.html
        # http://docs.python-requests.org/en/master/user/quickstart
        files = kw.pop("files", None) if is_aiohttp else None

        if self.is_form:
            if isinstance(data, dict) and isinstance(files, dict):
                data.update(files)
        else:
            kw['data'] = related.to_json(data)

        # unlimited timeout if not specified
        kw.setdefault("timeout", None)

        # add verify = False for requests
        if not is_aiohttp:
            kw['verify'] = False

        return kw


@related.mutable
class StepResult(object):
    step = related.ChildField(Step)
    success = related.BooleanField(default=True)
    fetch = related.ChildField(Fetch, required=False)
    response = related.ChildField(object, required=False)
    transform = related.ChildField(object, required=False)
    extract = related.ChildField(Namespace, required=False)
    status = related.IntegerField(required=False)
    validations = related.SequenceField(ValidationResult, required=False)
    duration = related.IntegerField(required=False)

    @property
    def failed_validations(self):
        return [v for v in self.validations if not v.success]


@related.mutable
class ScenarioResult(object):
    uuid = related.UUIDField()
    case = related.ChildField(Case, required=None)
    scenario = related.ChildField(Namespace, required=None)
    success = related.BooleanField(default=True)
    step_results = related.SequenceField(StepResult, default=[])
    suite = related.ChildField(Suite, required=False)


@related.immutable
class CaseResult(object):
    suite = related.ChildField(Suite)
    case = related.ChildField(Case)
    passed = related.SequenceField(ScenarioResult, default=[])
    failed = related.SequenceField(ScenarioResult, default=[])

    @property
    def success(self):
        return bool(self.passed and not self.failed)


@related.immutable
class SuiteResult(object):
    suite = related.ChildField(Suite)
    passed = related.SequenceField(CaseResult, default=[])
    failed = related.SequenceField(CaseResult, default=[])

    @property
    def success(self):
        return bool(self.passed and not self.failed)

    @classmethod
    def create(cls, suite, scenario_results):
        cache = {}

        for result in scenario_results:
            case_result = cache.setdefault(
                result.case.uuid,
                CaseResult(suite=suite, case=result.case)
            )

            sink = case_result.passed if result.success else case_result.failed
            sink.append(result)

        passed, failed = [], []
        for case_result in cache.values():
            sink = passed if case_result.success else failed
            sink.append(case_result)

        return cls(suite=suite, passed=passed, failed=failed)


@related.mutable
class State(ScenarioResult, Timer):
    session = related.ChildField(Session, required=False)
    globals = related.ChildField(Namespace, default=Namespace())
    extract = related.ChildField(Namespace, default=Namespace())
    iterate = related.ChildField(Namespace, default=Namespace())
    response = related.ChildField(Namespace, default=Namespace())
    transform = related.ChildField(Namespace, default=Namespace())

    def __attrs_post_init__(self):
        if self.suite is None:
            self.suite = self.session.suite if self.session else None

        globals = self.suite.globals if self.suite else {}
        globals = globals or {}
        self.globals = Namespace(globals)

        if self.scenario:
            self.scenario = self.scenario.evaluate(self.namespace)
        else:
            self.scenario = Namespace({})

    @property
    def namespace(self):
        # make globals namespace top-level
        values = self.globals.copy()

        # make scenario namespace top-level accessors (overrides globals!)
        values.update(self.scenario if self.scenario else {})

        # make iterate namespace top-level accessors (overrides scenario!)
        values.update(self.iterate if self.iterate else {})

        # make extract namespace top-level accessors (overrides iterate!)
        values.update(self.extract if self.extract else {})

        # add handles to namespaces (overrides scenario, extract and iterate!)
        values.update(dict(__uuid__=self.uuid,
                           globals=self.globals,
                           scenario=self.scenario,
                           transform=self.transform,
                           extract=self.extract,
                           response=self.response,
                           iterate=self.iterate,
                           env=os.environ))

        return values

    def should_run_step(self, step):
        condition = step.condition  # todo: evaluation here
        return self.success if condition is None else condition

    def do_transform(self, step):
        # make request and store response
        if step.transform:
            transform = Namespace.render(step.transform, self.namespace)
            output = jmespath.search(transform, self.response)
            get_logger().debug("transform",
                               response=self.response,
                               original=self.transform,
                               input=transform,
                               output=transform)
            return output

    def do_extract(self, step):
        existing = related.to_dict(self.extract) if self.extract else {}
        return step.extract.evaluate(self.namespace, existing)

    def do_validate(self, step, status):
        results = []

        # status check
        expected_status = step.request.status or HTTP_SUCCESS
        validator = Validator(actual="${status}",
                              expect=expected_status,
                              compare=enums.Comparison.IN)
        results.append(ValidationResult(actual=status,
                                        expect=expected_status,
                                        success=status in expected_status,
                                        validator=validator))

        # validators check
        for validator in step.validate or []:
            results.append(self.check_validation(validator))

        # determine success and return
        success = True
        for validator in results:
            kw = dict(validator=validator, step=step)
            log_with_success("validator", validator.success, **kw)
            success = success and validator.success

        return results, success

    def is_feature_table(self, s):
        return isinstance(s, str) and s.strip().startswith("|") and "\n" in s

    def check_validation(self, validator):
        actual, expect = validator.actual, validator.expect

        if self.is_feature_table(expect):
            expect = utils.parse_into_rows_of_dicts(expect)

        actual = Namespace.render(actual, self.namespace)
        expect = Namespace.render(expect, self.namespace)

        compare = Namespace.render(validator.compare, self.namespace)
        compare = related.to_model(enums.Comparison, compare)
        success = compare.evaluate(actual, expect)

        return ValidationResult(actual=actual, expect=expect, success=success,
                                validator=validator)

    def add_step(self, step_result):
        self.step_results.append(step_result)
        self.success = self.success and step_result.success

    def result(self):
        log_with_success("scenario", self.success,
                         feature=self.case.name,
                         scenario=self.scenario.__name__,
                         file_path=self.case.file_path,
                         num_steps=len(self.step_results),
                         timer=self.get_duration())

        return ScenarioResult(
            uuid=self.uuid,
            suite=self.suite,
            case=self.case,
            scenario=self.scenario,
            success=self.success,
            step_results=self.step_results
        )


@related.mutable
class StepState(StepResult, Timer):
    state = related.ChildField(State, required=False)
    retry = related.IntegerField(required=False, default=0)

    @property
    def namespace(self):
        return self.state.namespace

    @property
    def request(self):
        return self.step.request

    @property
    def suite(self):
        return self.state.suite

    @property
    def case(self):
        return self.state.case

    @property
    def host(self):
        # url host (request > case > cli > rigor.yml > localhost:8000)
        return self.request.host or self.case.host or self.suite.host

    @property
    def url(self):
        path = Namespace.render(self.request.path, self.namespace)
        return "%s/%s" % (self.host, path)

    @property
    def method(self):
        return self.request.method.value.lower()

    def get_data(self):
        return self.request.get_data(self.namespace)

    def get_files(self):
        return self.request.get_files(self.case.dir_path, self.namespace)

    def get_headers(self, content_type=None):
        headers = {}

        # right now
        if content_type:
            headers[const.CONTENT_TYPE] = content_type

        headers.update(related.to_dict(self.suite.headers) or {})

        if self.case:
            headers.update(related.to_dict(self.case.headers) or {})

        headers.update(related.to_dict(self.request.headers) or {})

        return Namespace(headers).evaluate(self.namespace)

    def get_fetch(self):
        if self.fetch is None:
            data, is_form = self.get_data()

            # libraries (requests, aiohttp) take care of CT for forms
            content_type = None if is_form else "application/json"
            headers = self.get_headers(content_type)

            params = self.request.get_params(self.namespace)
            files = self.get_files()
            kw = dict(headers=headers, data=data, params=params, files=files)
            self.fetch = Fetch(method=self.method, url=self.url, kwargs=kw,
                               is_form=is_form)

        return self.fetch

    def process_response(self, response, status):
        state = self.state

        # capture response
        self.response = state.response = response
        self.status = status

        # transform response
        self.transform = state.transform = state.do_transform(self.step)

        # extract response
        self.extract = state.extract = state.do_extract(self.step)

        # validate response
        self.validations, self.success = state.do_validate(self.step, status)

        # track overall success
        state.success = state.success and self.success

    def result(self):
        return StepResult(
            step=self.step,
            success=self.success,
            fetch=self.fetch,
            response=self.response,
            transform=self.transform,
            extract=self.extract,
            status=self.status,
            validations=self.validations,
            duration=self.get_duration(),
        )

    @property
    def sleep(self):
        return self.step.sleep + (self.retry * self.state.suite.sleep)
