"""Consume real producer output/status, then select only the app's own source plist."""
from contextlib import redirect_stderr
import io
import json
import os
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch

import resolve_info_plist as subject


class ResolveInfoPlistTest(unittest.TestCase):
    def setUp(self):
        self.temp = tempfile.TemporaryDirectory()
        self.addCleanup(self.temp.cleanup)
        self.root = Path(self.temp.name)
        self.source = self.root / 'Nested Project' / 'Application Info.plist'
        self.source.parent.mkdir()
        self.source.write_text('<plist/>')
        self.values = {'PRODUCT_TYPE': subject.APP_TYPE, 'PRODUCT_BUNDLE_IDENTIFIER': 'test.local.app',
                       'SRCROOT': str(self.source.parent), 'INFOPLIST_FILE': self.source.name}

    def producer(self, body):
        tool = self.root / 'xcodebuild'
        tool.write_text('#!/usr/bin/env python3\n' + body)
        tool.chmod(0o700)
        return patch.dict(os.environ, {'PATH': str(self.root) + os.pathsep + os.environ['PATH']})

    def test_drains_large_output_and_preserves_arguments_and_inherited_proxy(self):
        record = self.root / 'invocation.json'
        rows = [{'buildSettings': dict(self.values, LARGE='x' * 500000)}]
        body = ("import json,os,sys\n"
                f"open({str(record)!r},'w').write(json.dumps([sys.argv[1:],os.getenv('HTTPS_PROXY')]))\n"
                f"sys.stdout.write({json.dumps(rows)!r})\n")
        with self.producer(body), patch.dict(os.environ, {'HTTPS_PROXY': 'http://fixture-proxy.invalid:9000'}):
            result = subject.read_settings('App.xcodeproj', '', 'App', 'Release', self.root / 'logs')
        self.assertEqual(result, rows)
        args, proxy = json.loads(record.read_text())
        self.assertEqual(proxy, 'http://fixture-proxy.invalid:9000')
        self.assertIn('-disableAutomaticPackageResolution', args)
        self.assertIn('-skipPackageUpdates', args)
        self.assertIn('CODE_SIGNING_ALLOWED=NO', args)
        self.assertEqual(args[-2:], ['-showBuildSettings', '-json'])
        self.assertNotIn('-allowProvisioningUpdates', args)

    def test_exit_74_is_not_an_empty_success_and_error_is_bounded_private(self):
        stderr = io.StringIO()
        error = 'x' * 20000 + '\nCould not resolve package: https://private.invalid/token-secret token-secret\n'
        body = f"import sys\nsys.stderr.write({error!r})\nsys.stdout.write('[]')\nsys.exit(74)\n"
        with self.producer(body), patch.dict(os.environ, {'APP_SECRET': 'token-secret'}), redirect_stderr(stderr):
            with self.assertRaises(subject.SettingsFailed) as raised:
                subject.read_settings('App.xcodeproj', '', 'App', 'Release', self.root / 'logs')
        self.assertEqual(raised.exception.code, 74)
        self.assertIn('Could not resolve package', stderr.getvalue())
        self.assertNotIn('token-secret', stderr.getvalue())
        self.assertNotIn('private.invalid', stderr.getvalue())
        self.assertLess(len(stderr.getvalue()), 8200)
        self.assertTrue((self.root / 'logs' / 'build-settings.stderr').is_file())
        for output in ['not-json', '{}']:
            with self.producer(f"print({output!r})\n"):
                with self.assertRaises(subject.SettingsFailed):
                    subject.read_settings('App.xcodeproj', '', 'App', 'Release', self.root / 'logs')

    def test_selects_bundle_matched_application_not_first_dependency_or_other_app(self):
        dependency = dict(self.values, PRODUCT_TYPE='com.apple.product-type.framework', INFOPLIST_FILE='Other.plist')
        other = dict(self.values, PRODUCT_BUNDLE_IDENTIFIER='test.local.other')
        rows = [{'buildSettings': row} for row in [dependency, other, self.values]]
        chosen = subject.application(rows, 'test.local.app')
        self.assertEqual(subject.source_plist(chosen, self.root), self.source.resolve())
        with self.assertRaises(subject.SettingsFailed):
            subject.application(rows, '')
        with self.assertRaises(subject.SettingsFailed):
            subject.application(rows, 'test.local.absent')

    def test_ambiguous_same_bundle_and_malformed_targets_refuse(self):
        for rows in [[{'buildSettings': self.values}] * 2, [{'buildSettings': []}], [None]]:
            with self.assertRaises(subject.SettingsFailed):
                subject.application(rows, 'test.local.app')

    def test_relative_and_variable_plist_paths_use_the_target_root(self):
        values = dict(self.values, INFOPLIST_FILE='$(SRCROOT)/Application Info.plist')
        self.assertEqual(subject.source_plist(values, self.root), self.source.resolve())
        values['INFOPLIST_FILE'] = '${UNKNOWN}/Info.plist'
        with self.assertRaises(subject.SettingsFailed):
            subject.source_plist(values, self.root)
        newline_root = self.root / 'project\nname'
        newline_root.mkdir()
        (newline_root / self.source.name).write_text('<plist/>')
        values = dict(self.values, SRCROOT=str(newline_root))
        with self.assertRaises(subject.SettingsFailed):
            subject.source_plist(values, self.root)

    def test_missing_outside_and_generated_paths_are_distinct(self):
        for values in [dict(self.values, INFOPLIST_FILE='Missing.plist'),
                       dict(self.values, INFOPLIST_FILE='/etc/hosts'),
                       dict(self.values, INFOPLIST_FILE='')]:
            with self.assertRaises(subject.SettingsFailed):
                subject.source_plist(values, self.root)
        generated = dict(self.values, INFOPLIST_FILE='', GENERATE_INFOPLIST_FILE='YES')
        self.assertIsNone(subject.source_plist(generated, self.root))

    def test_timeout_keeps_error_evidence_and_does_not_retry(self):
        import subprocess

        stderr = io.StringIO()
        def timed_out(command, **kwargs):
            kwargs['stderr'].write(b'package graph was unavailable')
            raise subprocess.TimeoutExpired(command, 300)
        with patch.object(subject.subprocess, 'run', side_effect=timed_out) as run, redirect_stderr(stderr):
            with self.assertRaises(subject.SettingsFailed) as raised:
                subject.read_settings('App.xcodeproj', '', 'App', 'Release', self.root / 'logs')
        self.assertEqual(raised.exception.code, 124)
        self.assertEqual(run.call_count, 1)
        self.assertIn('package graph was unavailable', stderr.getvalue())


if __name__ == '__main__':
    unittest.main()
