# This file is part of the Python aiocoap library project.
#
# Copyright (c) 2012-2014 Maciej Wasilak <http://sixpinetrees.blogspot.com/>,
#               2013-2014 Christian Amsüss <c.amsuess@energyharvesting.at>
#
# aiocoap is free software, this file is published under the MIT license as
# described in the accompanying LICENSE file.

"""Tests for the file server.

This expects a file server at a CoAP server's root directory to be empty; files
are created, and different ways of triggering conflict responses are triggered
(eg. by overwriting a file, and then attempting to overwrite it with a
different one expecting the old state).
"""

import argparse
import asyncio
import os
import sys
import tempfile
import unittest

import aiocoap
from aiocoap import Message
from aiocoap.numbers.codes import *
from aiocoap.util import hostportjoin

from .common import PYTHON_PREFIX, CapturingSubprocess
from .test_server import WithAsyncLoop, WithClient, asynctest

SERVER_NETLOC = hostportjoin('::1', None)
AIOCOAP_FILESERVER = PYTHON_PREFIX + ['./aiocoap-fileserver', '--write', '--bind', SERVER_NETLOC]

@unittest.skipIf(aiocoap.defaults.linkheader_missing_modules(), "Module missing for running fileserver tests: %s"%(aiocoap.defaults.linkheader_missing_modules(),))
class WithFileServer(WithAsyncLoop):
    def setUp(self):
        super().setUp()
        ready = self.loop.create_future()
        self.__done = self.loop.create_future()

        self.filedir = tempfile.mkdtemp(suffix="-fileserver")

        self.__task = self.loop.create_task(self.run_server(ready, self.__done))
        self.loop.run_until_complete(ready)

    # This might be overly complex; it was stripped down from the more intricate OSCORE plug tests
    async def run_server(self, readiness, done):
        self.process, process_outputs = await self.loop.subprocess_exec(
                CapturingSubprocess,
                *(AIOCOAP_FILESERVER + ["-vvvvvvvv"]),
                self.filedir,
                stdin=None
                )

        while True:
            if b"Server ready to receive requests" in process_outputs.stderr:
                break
            if self.process.get_returncode() is not None:
                readiness.set_exception(RuntimeError(
                    "File server process terminated during startup:\n%s\n%s" % (
                        process_outputs.stdout.decode('utf8'),
                        process_outputs.stderr.decode('utf8'))))
                return
            await process_outputs.read_more
        readiness.set_result(True)

        while True:
            if self.process.get_returncode() is not None:
                break
            await process_outputs.read_more

        done.set_result(
                (self.process.get_returncode(), process_outputs.stdout, process_outputs.stderr))

        self.process.close()

    def tearDown(self):
        # Don't leave this over, even if anything is raised during teardown
        self.process.terminate()

        # Checking whether it's empty
        os.rmdir(self.filedir)

        super().tearDown()

        code, out, err = self.loop.run_until_complete(self.__done)

class TestFileServer(WithFileServer, WithClient):
    @asynctest
    async def test_fullcycle(self):
        await work_fileserver(self.client, "coap://%s/" % SERVER_NETLOC, self.assertTrue)

# Implemented as a single function to ease use from the command line
async def work_fileserver(ctx, base_uri, assert_):
    assert_(base_uri.endswith('/') and base_uri.count('/') == 3,
            "Base URI needs to be of shape coapsomething://hostname/ "
            "(with precisely these slashes)")

    req = Message(code=GET, uri=base_uri)
    res = await ctx.request(req).response_raising
    assert_(res.code == CONTENT)
    assert_(res.opt.content_format == 40, "Directory listing is not in link-format")
    assert_(res.payload == b"", "Directory is initially not empty")

    file1_body = b"Hello World\n" * 100
    req = Message(code=PUT, uri=base_uri + "file", payload=file1_body)
    res = await ctx.request(req).response_raising
    assert_(res.code == CHANGED)
    early_etag = res.opt.etag
    assert_(early_etag is not None, "PUT did not already send an ETag")

    # TBD: Could persist a content format

    req = Message(code=GET, uri=base_uri + "file")
    res = await ctx.request(req).response_raising
    assert_(res.code == CONTENT)
    assert_(res.payload == file1_body)
    etag1 = res.opt.etag
    assert_(etag1 == early_etag, "PUT response ETag is not GET response ETag")
    assert_(etag1 is not None, "No ETag returned")

    # Revalidate
    req = Message(code=GET, uri=base_uri + "file", etags=[b"synthetic", etag1])
    res = await ctx.request(req).response_raising
    assert_(res.code == VALID)
    assert_(not res.payload)
    assert_(res.opt.etag == etag1)

    file2_body = b"It is diffrent now."
    req = Message(code=PUT, uri=base_uri + "file", payload=file2_body, if_none_match=True)
    res = await ctx.request(req).response
    assert_(res.code == PRECONDITION_FAILED, "Overwrite succeeded even though expected empty")

    req = Message(code=PUT, uri=base_uri + "file", payload=file2_body, if_match=[b"synthetic"])
    res = await ctx.request(req).response
    assert_(res.code == PRECONDITION_FAILED, "Overwrite succeeded even though expected to conflict")

    req = Message(code=PUT, uri=base_uri + "file", payload=file2_body, if_match=[etag1])
    res = await ctx.request(req).response_raising
    assert_(res.code == CHANGED)

    req = Message(code=GET, uri=base_uri + "file")
    res = await ctx.request(req).response_raising
    assert_(res.code == CONTENT)
    assert_(res.payload == file2_body)
    etag2 = res.opt.etag
    assert_(etag2 is not None, "No ETag returned")

    req = Message(code=DELETE, uri=base_uri + "file", if_match=[b"not the one"])
    res = await ctx.request(req).response
    assert_(res.code == PRECONDITION_FAILED, "Deletion should not pass with a non-matching ETag")

    # Actual cleanup
    req = Message(code=DELETE, uri=base_uri + "file", if_match=[etag2])
    res = await ctx.request(req).response_raising
    assert_(res.code == DELETED)

    # Just to be sure
    req = Message(code=DELETE, uri=base_uri + "file")
    res = await ctx.request(req).response
    assert_(res.code == NOT_FOUND) # DELETED might be OK too under idempotency rules?

async def run_standalone():
    p = argparse.ArgumentParser(description=__doc__)
    p.add_argument("base_uri")
    args = p.parse_args()

    ctx = await aiocoap.Context.create_client_context()
    def assert_(truth, error=""):
        if not truth:
            print("Assertion failed:", error)
            sys.exit(1)
    await work_fileserver(ctx, args.base_uri, assert_)

if __name__ == "__main__":
    # due to the imports, you'll need to run this as `python3 -m tests.test_fileserver`
    asyncio.run(run_standalone())
