import unittest
import json
import os
import sys
from subprocess import Popen, PIPE
from genson import SchemaBuilder

BASE_SCHEMA = {"$schema": SchemaBuilder.DEFAULT_URI}
FIXTURE_PATH = os.path.join(os.path.dirname(__file__), 'fixtures')
SHORT_USAGE = """\
usage: genson [-h] [--version] [-d DELIM] [-e ENCODING] [-i SPACES]
              [-s SCHEMA] [-$ SCHEMA_URI]
              ..."""


def fixture(filename):
    return os.path.join(FIXTURE_PATH, filename)


def stderr_message(message):
    return '{}\ngenson: error: {}\n'.format(SHORT_USAGE, message)


def run(args=tuple(), stdin_data=None, tty_stdin=False):
    """
    Run the ``genson`` executable as a subprocess and return
    (stdout, stderr). ``tty_stdin`` attaches stdin to a pseudo-TTY so
    the CLI treats the session as interactive; otherwise stdin is a
    pipe (with ``stdin_data`` written to it, if given).
    """
    full_args = [sys.executable, '-m', 'genson']
    full_args.extend(args)
    env = os.environ.copy()
    env['COLUMNS'] = '80'  # set width for deterministic text wrapping

    if tty_stdin:
        import pty
        master_fd, slave_fd = pty.openpty()
        stdin = slave_fd
    else:
        master_fd = slave_fd = None
        stdin = PIPE if stdin_data is not None else None

    genson_process = Popen(
        full_args, env=env, stdout=PIPE, stderr=PIPE, stdin=stdin)
    if stdin_data is not None:
        stdin_data = stdin_data.encode('utf-8')
    (stdout, stderr) = genson_process.communicate(stdin_data)
    genson_process.wait()
    if slave_fd is not None:
        os.close(slave_fd)
        os.close(master_fd)

    if isinstance(stdout, bytes):
        stdout = stdout.decode('utf-8')
    if isinstance(stderr, bytes):
        stderr = stderr.decode('utf-8')
    return (stdout, stderr)


class TestBasic(unittest.TestCase):

    def test_empty_input(self):
        (stdout, stderr) = run(stdin_data='')
        self.assertEqual(stderr, '')
        self.assertEqual(json.loads(stdout), BASE_SCHEMA)

    def test_empty_object_stdin(self):
        (stdout, stderr) = run(stdin_data='{}')
        self.assertEqual(stderr, '')
        self.assertEqual(
            json.loads(stdout),
            dict({"type": "object"}, **BASE_SCHEMA))

    def test_empty_object_file(self):
        (stdout, stderr) = run([fixture('empty.json')])
        self.assertEqual(stderr, '')
        self.assertEqual(
            json.loads(stdout),
            BASE_SCHEMA)

    def test_basic_schema_file(self):
        (stdout, stderr) = run(['-s', fixture('base_schema.json')])
        self.assertEqual(stderr, '')
        self.assertEqual(
            json.loads(stdout),
            BASE_SCHEMA)


class TestError(unittest.TestCase):
    maxDiff = 1000
    BAD_JSON_FILE = fixture('not_json.txt')
    BAD_JSON_MESSAGE = stderr_message(
        'invalid JSON in %s: Expecting value: line 1 column 1 (char 0)'
        % BAD_JSON_FILE)

    def assertEqualIgnoreWhitespace(
        self,
        first: str,
        second: str,
        msg: "str|None" = None,
    ):
        return self.assertEqual(
            " ".join(first.split()),
            " ".join(second.split()),
            msg=msg
        )

    @unittest.skipUnless(os.name == 'posix', 'pty requires a POSIX system')
    def test_no_input(self):
        # attach a pseudo-TTY so the CLI sees an interactive session
        # with no piped input rather than inheriting the runner's stdin
        (stdout, stderr) = run(tty_stdin=True)
        self.assertEqualIgnoreWhitespace(stderr, stderr_message(
            'nothing to do - no schemas or objects given'))
        self.assertEqual(stdout, '')

    def test_object_not_json(self):
        (stdout, stderr) = run([self.BAD_JSON_FILE])
        self.assertEqualIgnoreWhitespace(stderr, self.BAD_JSON_MESSAGE)
        self.assertEqual(stdout, '')

    def test_schema_not_json(self):
        (stdout, stderr) = run(['-s', self.BAD_JSON_FILE])
        self.assertEqualIgnoreWhitespace(stderr, self.BAD_JSON_MESSAGE)
        self.assertEqual(stdout, '')


class TestDelimiter(unittest.TestCase):

    def test_delim_newline(self):
        (stdout, stderr) = run(['-d', 'newline'],
                               stdin_data='{"hi":"there"}\n{"hi":5}')
        self.assertEqual(stderr, '')
        self.assertEqual(
            json.loads(stdout),
            dict({"required": ["hi"], "type": "object", "properties": {
                "hi": {"type": ["integer", "string"]}}}, **BASE_SCHEMA))

    def test_delim_auto_empty(self):
        (stdout, stderr) = run(['-d', ''], stdin_data='{"hi":"there"}{"hi":5}')
        self.assertEqual(stderr, '')
        self.assertEqual(
            json.loads(stdout),
            dict({"required": ["hi"], "type": "object", "properties": {
                "hi": {"type": ["integer", "string"]}}}, **BASE_SCHEMA))

    def test_delim_auto_whitespace(self):
        (stdout, stderr) = run(['-d', ''],
                               stdin_data='{"hi":"there"} \n\t{"hi":5}')
        self.assertEqual(stderr, '')
        self.assertEqual(
            json.loads(stdout),
            dict({"required": ["hi"], "type": "object", "properties": {
                "hi": {"type": ["integer", "string"]}}}, **BASE_SCHEMA))


class TestEncoding(unittest.TestCase):

    def test_encoding_unicode(self):
        (stdout, stderr) = run(
            ['-e', 'utf-8', fixture('utf-8.json')])
        self.assertEqual(stderr, '')
        self.assertEqual(
            json.loads(stdout),
            dict({"type": "string"}, **BASE_SCHEMA))

    def test_encoding_cp1252(self):
        (stdout, stderr) = run(
            ['-e', 'cp1252', fixture('cp1252.json')])
        self.assertEqual(stderr, '')
        self.assertEqual(
            json.loads(stdout),
            dict({"type": "string"}, **BASE_SCHEMA))
