################################################################
#
#  Licensed to the Apache Software Foundation (ASF) under one
#  or more contributor license agreements.  See the NOTICE file
#  distributed with this work for additional information
#  regarding copyright ownership.  The ASF licenses this file
#  to you under the Apache License, Version 2.0 (the
#  "License"); you may not use this file except in compliance
#  with the License.  You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing,
#  software distributed under the License is distributed on an
#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
#  KIND, either express or implied.  See the License for the
#  specific language governing permissions and limitations
#  under the License.
#
################################################################

"""Shared pytest configuration for the AOO test suite."""

import os

import pytest

from aootest import documents, expect, install, pipeline, session, twins
from aootest.odfversion import SAVE_MODES, odf_versions


def pytest_addoption(parser):
    g = parser.getgroup("aoo", "AOO test suite")
    g.addoption("--office", default=None, help="an AOO 5+ install: .app, install root, program dir or soffice (default: $AOO_OFFICE)")
    g.addoption("--uno-python", default=None, help="Python that can import uno (default: $AOO_UNO_PYTHON, else probed)")
    g.addoption("--connect", choices=("socket", "pipe"), default="socket", help="how the worker reaches the office")
    g.addoption("--start-timeout", type=float, default=240.0, help="seconds an office may take to start and accept a connection (default %(default)s)")
    g.addoption("--max-restarts", type=int, default=5, help="office restarts allowed per mode before the session aborts")
    g.addoption("--require-office", action="store_true", help="fail instead of skipping when no usable office is found")
    g.addoption("--documents", default=None, help="a directory of ODF documents to run through every save mode, in addition to the ones the office ships")
    g.addoption("--shipped-limit", type=int, default=documents.DEFAULT_LIMIT, help="how many of the templates and documents shipped with the office to use (default %(default)s)")
    g.addoption("--twin-corpus", default=None, help="a directory with v12, v13, v12ext and v13ext subdirectories holding the same documents saved as ODF 1.2 and 1.3 by another producer (default: $AOO_TWIN_CORPUS)")
    g.addoption("--strict-expect", action="store_true", help="an item that a data/expect row expects to fail must fail (XPASS becomes a failure)")
    g.addoption("--write-kinds", action="store_true", help="write data/baseline/ from this run instead of comparing with it (tests/odf/test_kinds.py)")
    g.addoption("--require-odf13", action="store_true", help="fail instead of skipping the ODF 1.3 save modes when the office cannot write 1.3")
    g.addoption("--require-odf14", action="store_true", help="fail instead of skipping the ODF 1.4 save modes when the office cannot write 1.4")


def _unavailable(config, message):
    if config.getoption("--require-office"):
        pytest.fail(message, pytrace=False)
    pytest.skip(message)


@pytest.fixture(scope="session")
def aoo_layout(pytestconfig):
    """The install named by --office or AOO_OFFICE. There is no search of default places: an office is only started on request."""
    location = office_location(pytestconfig)
    if not location:
        _unavailable(pytestconfig, "set AOO_OFFICE or --office to an installed AOO 5 to run office tests")
    try:
        return install.discover(location)
    except install.LayoutError as e:
        _unavailable(pytestconfig, "no usable office at %s: %s" % (location, e))


@pytest.fixture(scope="session")
def aoo_python(pytestconfig, aoo_layout, tmp_path_factory):
    try:
        return install.find_uno_python(aoo_layout, explicit=pytestconfig.getoption("--uno-python"),
                                       cache_file=tmp_path_factory.getbasetemp() / "uno-python.json")
    except install.NoUnoPython as e:
        _unavailable(pytestconfig, "no UNO Python for this install: %s; tried %s" % (e, e.tried))


class Offices:
    """One lazily started office and worker per ODF mode (None = product default, or a Save/ODF/DefaultVersion number)."""

    def __init__(self, layout, python, workdir, connect, max_restarts, start_timeout=240.0):
        self.layout, self.python, self.workdir, self.connect, self.max_restarts = layout, python, workdir, connect, max_restarts
        self.start_timeout = start_timeout
        self.sessions = {}

    def get(self, mode=None, extra=None, tag=None):
        """The session for `mode`; `extra` (other Save/ODF settings) needs a `tag` that names it."""
        key = (mode, tag)
        if key not in self.sessions:
            self.sessions[key] = session.ModeSession(self.layout, self.python, mode, self.workdir, self.connect, self.max_restarts,
                                                     connect_timeout=self.start_timeout, extra=extra, tag=tag)
        return self.sessions[key]

    def close(self):
        for s in self.sessions.values():
            s.close()
        left = [(s.name, o.pid) for s in self.sessions.values() for o in s.started if o.alive()]
        assert not left, "offices still running after the session: %s" % left


@pytest.fixture(scope="session")
def aoo_offices(pytestconfig, aoo_layout, aoo_python, tmp_path_factory):
    offices = Offices(aoo_layout, aoo_python, tmp_path_factory.mktemp("aoo"), pytestconfig.getoption("--connect"),
                      pytestconfig.getoption("--max-restarts"), pytestconfig.getoption("--start-timeout"))
    yield offices
    offices.close()


def office_location(config):
    return config.getoption("--office") or os.environ.get("AOO_OFFICE")


@pytest.fixture(scope="session")
def odf_runs(pytestconfig, aoo_offices, tmp_path_factory):
    """{mode name: ModeRun}, created on first use: `odf_runs("strict")`."""
    work = tmp_path_factory.mktemp("odf")
    runs = {}
    sources = documents.from_config(pytestconfig)
    # the sources are made in ODF 1.2 extended mode, which keeps every feature the reload checks look for
    prepared = documents.Prepared(aoo_offices.get(SAVE_MODES["ext"]), work / "src",
                                  lambda builder: aoo_offices.get(SAVE_MODES["ext"], tag="build-" + builder))

    def get(name):
        if name not in runs:
            runs[name] = pipeline.ModeRun(name, aoo_offices.get(SAVE_MODES[name]), work, sources, prepared)
        return runs[name]
    return get


@pytest.fixture(scope="session")
def twin_run(pytestconfig, aoo_offices, tmp_path_factory):
    """The TwinRun over the corpus named by --twin-corpus or AOO_TWIN_CORPUS; skips when there is none."""
    root = pytestconfig.getoption("--twin-corpus") or os.environ.get("AOO_TWIN_CORPUS")
    if not root:
        pytest.skip("set AOO_TWIN_CORPUS or --twin-corpus to a corpus of ODF 1.2 and 1.3 twins to run the import twin tests")
    problem = twins.corpus_problem(root)
    if problem:
        pytest.fail("twin corpus: " + problem, pytrace=False)
    return twins.TwinRun(aoo_offices.get(None), root, tmp_path_factory.mktemp("twins"))


_EXPECT_ROWS = []


def pytest_collection_modifyitems(config, items):
    rows = _EXPECT_ROWS[:] or expect.load()
    _EXPECT_ROWS[:] = rows
    strict = config.getoption("--strict-expect")
    for item in items:
        row = expect.match(rows, item.name)
        if row is None:
            continue
        row.used += 1
        if row.kind in ("xfail", "flaky"):
            item.add_marker(pytest.mark.xfail(reason=row.reason, strict=strict and row.kind == "xfail"))
        else:
            item.add_marker(pytest.mark.skip(reason=row.reason))


@pytest.fixture(scope="session")
def odf13(aoo_offices, tmp_path_factory):
    """Whether this build writes ODF 1.3 in save mode 5: builds from before the ODF 1.3 work ignore the setting."""
    dst = tmp_path_factory.mktemp("odf13") / "probe.odt"
    aoo_offices.get(5).call("new_and_store", {"app": "writer", "dst": str(dst), "filter": "writer8"}, timeout=120)
    return odf_versions(dst) == ("1.3", "1.3")


@pytest.fixture(scope="session")
def odf14(aoo_offices, tmp_path_factory):
    """Whether this build writes ODF 1.4 in save mode 7: builds from before the ODF 1.4 work ignore the setting."""
    dst = tmp_path_factory.mktemp("odf14") / "probe.odt"
    aoo_offices.get(7).call("new_and_store", {"app": "writer", "dst": str(dst), "filter": "writer8"}, timeout=120)
    return odf_versions(dst) == ("1.4", "1.4")


@pytest.fixture
def need_mode(request, pytestconfig):
    """need_mode(mode): skip (or fail with --require-odf13 or --require-odf14) when the build cannot write that save mode (names or numbers)."""
    def check(mode):
        number = SAVE_MODES.get(mode, mode)
        for numbers, probe, option, label in (((5, 6), "odf13", "--require-odf13", "1.3"), ((7, 8), "odf14", "--require-odf14", "1.4")):
            if number in numbers and not request.getfixturevalue(probe):
                message = "this build does not write ODF %s (save mode %s)" % (label, mode)
                if pytestconfig.getoption(option):
                    pytest.fail(message, pytrace=False)
                pytest.skip(message)
    return check


def pytest_terminal_summary(terminalreporter, config):
    unused = [r for r in _EXPECT_ROWS if not r.used]
    if unused and not config.option.keyword and not config.args[1:]:
        terminalreporter.write_line("expectation rows that matched no collected item: %d (first: %s at %s)" % (len(unused), unused[0].pattern, unused[0].origin))
