import contextlib
import io
import json
import os
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
from urllib.error import HTTPError

from secure_credential_validator import SecureCredentialValidator, load_private_credentials, Rejected
from check_live_status import read_json

KEY = 'AKTESTISOLATED12345'
SECRET = 'TestSecretIsolated98765'
PAPER_KEY = 'PKTESTISOLATED12345'


def account():
    return {'status':'ACTIVE','cash':'0','equity':'0','buying_power':'0',
            'trading_blocked':False,'account_blocked':False,'transfers_blocked':False,
            'trade_suspended_by_user':False}


class SecureCredentialTests(unittest.TestCase):
    def setUp(self):
        self.temporary = tempfile.TemporaryDirectory()
        self.root = Path(self.temporary.name)
        self.vault = self.root/'vault.env'
        self.settings = {'ALPACA_LIVE_KEY':KEY,'ALPACA_LIVE_SECRET':SECRET}
        self.calls = []

    def tearDown(self):
        self.temporary.cleanup()

    def file(self, text):
        self.vault.write_text(text)
        self.vault.chmod(0o600)

    def get(self,url,headers):
        self.calls.append((url,headers))
        return account()

    def validator(self, **kwargs):
        defaults = {'settings':self.settings,'get':self.get,'env_path':self.vault}
        defaults.update(kwargs)
        return SecureCredentialValidator(**defaults)

    def test_live_authentication_is_only_account_read(self):
        result=self.validator().authentication_report()
        self.assertEqual(result['status'],'ACCOUNT_READ_AUTHENTICATED')
        self.assertEqual(self.calls[0][0],'https://api.alpaca.markets/v2/account')
        self.assertFalse(result['credential_rotation_verified'])
        self.assertFalse(result['fresh_market_feed_verified'])
        self.assertFalse(result['futures_feed_verified'])
        self.assertFalse(result['live_clearance_granted'])
        self.assertFalse(result['trial_started'])

    def test_paper_credentials_stay_paper(self):
        result=self.validator(mode='paper',settings={'ALPACA_PAPER_KEY':PAPER_KEY,'ALPACA_PAPER_SECRET':SECRET}).authentication_report()
        self.assertEqual(result['status'],'ACCOUNT_READ_AUTHENTICATED')
        self.assertEqual(self.calls[0][0],'https://paper-api.alpaca.markets/v2/account')

    def test_paper_marker_blocks_live_request(self):
        self.settings['ALPACA_LIVE_KEY']=PAPER_KEY
        self.assertEqual(self.validator().authentication_report()['status'],'CONFIGURATION_BLOCKED')
        self.assertEqual(self.calls,[])

    def test_missing_configuration_does_not_seed_credentials(self):
        with patch.dict(os.environ,{},clear=True):
            result=self.validator(settings={},environment_only=True).authentication_report()
            self.assertEqual(dict(os.environ),{})
        self.assertEqual(result['status'],'CONFIGURATION_BLOCKED')
        self.assertEqual(self.calls,[])
        self.assertFalse(self.vault.exists())

    def test_mock_credentials_never_sent(self):
        for key in ['MOCK_KEY_VAL_9921','AK_REAL_PRODUCTION_KEY_VAL_9921','your_key_here']:
            result=self.validator(settings={'ALPACA_LIVE_KEY':key,'ALPACA_LIVE_SECRET':SECRET}).authentication_report()
            self.assertEqual(result['status'],'CONFIGURATION_BLOCKED')
        self.assertEqual(self.calls,[])

    def test_unsafe_endpoints_block_before_network(self):
        for endpoint in ['https://alpaca.markets','https://attacker.invalid','http://api.alpaca.markets',
                         'https://api.alpaca.markets@attacker.invalid','https://api.alpaca.markets/v2/orders',
                         'https://paper-api.alpaca.markets']:
            settings={**self.settings,'ALPACA_BASE_URL':endpoint}
            with self.subTest(endpoint=endpoint):
                self.assertEqual(self.validator(settings=settings).authentication_report()['status'],'CONFIGURATION_BLOCKED')
        self.assertEqual(self.calls,[])

    def test_file_endpoint_loaded_before_validation(self):
        self.file(f'export ALPACA_PAPER_KEY="{PAPER_KEY}"\nALPACA_PAPER_SECRET=\'{SECRET}\'\nexport ALPACA_BASE_URL="https://paper-api.alpaca.markets" # literal endpoint\n')
        result=self.validator(mode='paper',settings={}).authentication_report()
        self.assertEqual(result['status'],'ACCOUNT_READ_AUTHENTICATED')
        self.assertEqual(self.calls[0][0],'https://paper-api.alpaca.markets/v2/account')

    def test_file_does_not_mutate_environment_or_bytes(self):
        self.file(f'ALPACA_LIVE_KEY={KEY}\nALPACA_LIVE_SECRET={SECRET}\n')
        before=self.vault.read_bytes()
        with patch.dict(os.environ,{'UNCHANGED':'keep'},clear=True):
            self.assertEqual(self.validator(settings={}).authentication_report()['status'],'ACCOUNT_READ_AUTHENTICATED')
            self.assertEqual(dict(os.environ),{'UNCHANGED':'keep'})
        self.assertEqual(self.vault.read_bytes(),before)

    def test_partial_environment_pair_is_not_mixed_with_file(self):
        self.file(f'ALPACA_LIVE_KEY={KEY}\nALPACA_LIVE_SECRET={SECRET}\n')
        result=self.validator(settings={'ALPACA_LIVE_KEY':'AKDIFFERENT123456'}).authentication_report()
        self.assertEqual(result['status'],'CONFIGURATION_BLOCKED')
        self.assertEqual(self.calls,[])

    def test_explicit_file_source_does_not_merge_environment(self):
        self.file(f'ALPACA_PAPER_KEY={PAPER_KEY}\nALPACA_PAPER_SECRET={SECRET}\n')
        result=self.validator(mode='live',file_only=True).authentication_report()
        self.assertEqual(result['status'],'CONFIGURATION_BLOCKED')
        self.assertEqual(self.calls,[])

    def test_environment_only_never_opens_vault(self):
        with patch('secure_credential_validator.load_private_credentials',side_effect=AssertionError('must not open')):
            self.assertEqual(self.validator(environment_only=True).authentication_report()['status'],'ACCOUNT_READ_AUTHENTICATED')

    def test_public_file_permissions_block(self):
        self.file(f'ALPACA_LIVE_KEY={KEY}\nALPACA_LIVE_SECRET={SECRET}\n');self.vault.chmod(0o644)
        self.assertEqual(self.validator(settings={}).authentication_report()['status'],'CONFIGURATION_BLOCKED')
        self.assertEqual(self.calls,[])

    def test_symlink_blocks(self):
        self.file(f'ALPACA_LIVE_KEY={KEY}\nALPACA_LIVE_SECRET={SECRET}\n')
        linked=self.root/'linked';linked.symlink_to(self.vault)
        self.assertEqual(self.validator(settings={},env_path=linked).authentication_report()['status'],'CONFIGURATION_BLOCKED')
        self.assertEqual(self.calls,[])

    def test_hard_link_blocks(self):
        self.file(f'ALPACA_LIVE_KEY={KEY}\nALPACA_LIVE_SECRET={SECRET}\n')
        os.link(self.vault,self.root/'linked')
        self.assertEqual(self.validator(settings={}).authentication_report()['status'],'CONFIGURATION_BLOCKED')
        self.assertEqual(self.calls,[])

    def test_file_owner_checked(self):
        self.file(f'ALPACA_LIVE_KEY={KEY}\nALPACA_LIVE_SECRET={SECRET}\n')
        with patch('secure_credential_validator.os.geteuid',return_value=os.geteuid()+1):
            with self.assertRaises(Rejected):load_private_credentials(self.vault)

    def test_nonregular_file_blocks(self):
        self.assertEqual(self.validator(settings={},env_path=self.root).authentication_report()['status'],'CONFIGURATION_BLOCKED')
        self.assertEqual(self.calls,[])

    def test_invalid_literal_assignments_block(self):
        for text in ['PATH=/unsafe\n','ALPACA_LIVE_KEY=one\nALPACA_LIVE_KEY=two\n',
                     'ALPACA_LIVE_KEY="unclosed\n','ALPACA_LIVE_KEY=$(touch bad)\n',
                     'ALPACA_LIVE_KEY=`touch bad`\n','ALPACA_LIVE_KEY=one two\n',
                     'ALPACA_LIVE_KEY=AKTEST\\ISOLATED12345\n',
                     'source another-file\n']:
            self.file(text)
            self.assertEqual(self.validator(settings={}).authentication_report()['status'],'CONFIGURATION_BLOCKED')
        self.assertEqual(self.calls,[])
        self.assertFalse((self.root/'bad').exists())

    def test_oversize_file_blocks(self):
        self.file('#'+'x'*8192)
        self.assertEqual(self.validator(settings={}).authentication_report()['status'],'CONFIGURATION_BLOCKED')

    def test_output_omits_credentials_and_account_contents(self):
        def response(url,headers):
            result=account();result['status']=SECRET;result['id']=KEY;return result
        stdout=io.StringIO()
        with contextlib.redirect_stdout(stdout):
            self.assertTrue(self.validator(get=response).execute_blind_authentication_audit())
        self.assertNotIn(KEY,stdout.getvalue());self.assertNotIn(SECRET,stdout.getvalue())
        self.assertNotIn('reported_amounts',stdout.getvalue())

    def test_error_text_and_body_cannot_leak(self):
        def fail(url,headers):raise RuntimeError(KEY+SECRET)
        output=json.dumps(self.validator(get=fail).authentication_report())
        self.assertNotIn(KEY,output);self.assertNotIn(SECRET,output)
        self.assertIn('AUTHENTICATION_NOT_VERIFIED',output)

    def test_malformed_account_does_not_pass(self):
        for response in [{}, {'status':'ACTIVE'},[],None,{'status':'ACTIVE','cash':'NaN','equity':'0','buying_power':'0'}]:
            result=self.validator(get=lambda u,h:response).authentication_report()
            self.assertEqual(result['status'],'AUTHENTICATION_NOT_VERIFIED')

    def test_http_rejection_body_is_not_printed(self):
        error=HTTPError('https://api.alpaca.markets/v2/account',401,SECRET,{},io.BytesIO((KEY+SECRET).encode()))
        with patch('check_live_status.build_opener') as opener:
            opener.return_value.open.side_effect=error
            result=self.validator(get=read_json).authentication_report()
        output=json.dumps(result)
        self.assertNotIn(KEY,output);self.assertNotIn(SECRET,output)
        self.assertEqual(result['status'],'AUTHENTICATION_NOT_VERIFIED')

    def test_transport_is_get_and_disables_redirects(self):
        with patch('check_live_status.build_opener') as opener:
            opener.return_value.open.return_value.__enter__.return_value.read.return_value=json.dumps(account()).encode()
            result=self.validator(get=read_json).authentication_report()
        self.assertEqual(result['status'],'ACCOUNT_READ_AUTHENTICATED')
        request=opener.return_value.open.call_args.args[0]
        self.assertEqual(request.get_method(),'GET')
        self.assertEqual(request.full_url,'https://api.alpaca.markets/v2/account')
        self.assertEqual(opener.call_args.args[0].__name__,'NoRedirect')


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