# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------
import unittest
from unittest import mock
import os

from azure.mgmt.web import WebSiteManagementClient
from knack.util import CLIError
from azure.cli.command_modules.appservice.custom import (
    enable_zip_deploy_functionapp,
    enable_zip_deploy,
    enable_zip_deploy_flex,
    check_flex_app_after_deployment,
    add_remote_build_app_settings,
    remove_remote_build_app_settings,
    config_source_control,
    validate_app_settings_in_scm,
    update_container_settings_functionapp,
    list_function_keys)
from azure.cli.core.profiles import ResourceType
from azure.cli.core.azclierror import (AzureInternalError, UnclassifiedUserFault)
from azure.cli.core.azclierror import ResourceNotFoundError

TEST_DIR = os.path.abspath(os.path.join(os.path.abspath(__file__), '..'))


def _get_test_cmd():
    from azure.cli.core.mock import DummyCli
    from azure.cli.core import AzCommandsLoader
    from azure.cli.core.commands import AzCliCommand
    cli_ctx = DummyCli()
    loader = AzCommandsLoader(cli_ctx, resource_type=ResourceType.MGMT_APPSERVICE)
    cmd = AzCliCommand(loader, 'test', None)
    cmd.command_kwargs = {'resource_type': ResourceType.MGMT_APPSERVICE}
    cmd.cli_ctx = cli_ctx
    return cmd


def _get_zip_deploy_headers(username, password, cmd_mock_client):
    from urllib3.util import make_headers
    from azure.cli.core.util import get_az_user_agent

    headers = make_headers(basic_auth='{0}:{1}'.format(username, password))
    headers['Content-Type'] = 'application/octet-stream'
    headers['Cache-Control'] = 'no-cache'
    headers['User-Agent'] = get_az_user_agent()
    headers['x-ms-client-request-id'] = cmd_mock_client.data['headers']['x-ms-client-request-id']
    return headers


def _get_flex_zip_deploy_headers(cmd_mock_client):
    from urllib3.util import make_headers
    from azure.cli.core.util import get_az_user_agent

    headers = make_headers()
    headers['Authorization'] = "Bearer 1234"
    headers['Content-Type'] = 'application/zip'
    headers['Cache-Control'] = 'no-cache'
    headers['User-Agent'] = get_az_user_agent()
    headers['x-ms-client-request-id'] = cmd_mock_client.data['headers']['x-ms-client-request-id']
    return headers


class TestFunctionappMocked(unittest.TestCase):
    def setUp(self):
        self.client = WebSiteManagementClient(mock.MagicMock(), '123455678')

    @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory', autospec=True)
    @mock.patch('azure.cli.command_modules.appservice.custom.parse_resource_id')
    @mock.patch('azure.cli.command_modules.appservice.custom.is_flex_functionapp', return_value=False)
    @mock.patch('azure.cli.command_modules.appservice.custom.enable_zip_deploy')
    @mock.patch('azure.cli.command_modules.appservice.custom.add_remote_build_app_settings')
    def test_functionapp_zip_deploy_flow(self,
                                         add_remote_build_app_settings_mock,
                                         enable_zip_deploy_mock,
                                         is_flex_functionapp_mock,
                                         parse_resource_id_mock,
                                         web_client_factory_mock):
        cmd_mock = _get_test_cmd()
        cli_ctx_mock = mock.MagicMock()
        cmd_mock.cli_ctx = cli_ctx_mock

        # action
        enable_zip_deploy_functionapp(cmd_mock, 'rg', 'name', 'src', build_remote=True, timeout=None, slot=None)

        # assert
        parse_resource_id_mock.assert_called_once()
        enable_zip_deploy_mock.assert_called_once()
        add_remote_build_app_settings_mock.assert_called_once()

    @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory', autospec=True)
    @mock.patch('azure.cli.command_modules.appservice.custom.parse_resource_id')
    @mock.patch('azure.cli.command_modules.appservice.custom.is_flex_functionapp', return_value=False)
    @mock.patch('azure.cli.command_modules.appservice.custom.enable_zip_deploy')
    @mock.patch('azure.cli.command_modules.appservice.custom.remove_remote_build_app_settings')
    def test_functionapp_zip_deploy_flow(self,
                                         remove_remote_build_app_settings_mock,
                                         enable_zip_deploy_mock,
                                         is_flex_functionapp_mock,
                                         parse_resource_id_mock,
                                         web_client_factory_mock):
        cmd_mock = _get_test_cmd()
        cli_ctx_mock = mock.MagicMock()
        cmd_mock.cli_ctx = cli_ctx_mock

        # action
        enable_zip_deploy_functionapp(cmd_mock, 'rg', 'name', 'src', build_remote=False, timeout=None, slot=None)

        # assert
        parse_resource_id_mock.assert_called_once()
        enable_zip_deploy_mock.assert_called_once()
        remove_remote_build_app_settings_mock.assert_called_once()

    @mock.patch('azure.cli.command_modules.appservice.custom.check_language_runtime')
    @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory', autospec=True)
    @mock.patch('azure.cli.command_modules.appservice.custom.parse_resource_id')
    @mock.patch('azure.cli.command_modules.appservice.custom.validate_zip_deploy_app_setting_exists')
    @mock.patch('azure.cli.command_modules.appservice.custom.upload_zip_to_storage')
    @mock.patch('azure.cli.command_modules.appservice.custom.is_plan_consumption', return_value=True)
    @mock.patch('azure.cli.command_modules.appservice.custom.is_flex_functionapp', return_value=False)
    def test_functionapp_linux_consumption_non_remote_build(self,
                                                            is_flex_functionapp_mock,
                                                            is_plan_consumption_mock,
                                                            upload_zip_to_storage_mock,
                                                            validate_zip_deploy_app_setting_exists_mock,
                                                            parse_resource_id_mock,
                                                            web_client_factory_mock,
                                                            check_language_runtime_mock):
        # prepare
        cmd_mock = _get_test_cmd()
        cli_ctx_mock = mock.MagicMock()
        cmd_mock.cli_ctx = cli_ctx_mock

        appservice_mock = mock.Mock()
        appservice_mock.reserved = True  # Marked app service as Linux

        web_client_mock = mock.Mock()
        web_client_mock.web_apps = mock.Mock()
        web_client_mock.web_apps.get = mock.Mock(return_value=appservice_mock)
        web_client_factory_mock.return_value = web_client_mock

        # action
        # Linux Consumption app should use update-storage to deploy when not using remote build
        enable_zip_deploy_functionapp(cmd_mock, 'rg', 'name', 'src', build_remote=False, timeout=None, slot=None)

        # assert
        web_client_mock.web_apps.get.assert_called_with('rg', 'name')
        upload_zip_to_storage_mock.assert_called_with(cmd_mock, 'rg', 'name', 'src', None)

    @mock.patch('azure.cli.command_modules.appservice.custom.check_language_runtime')
    @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory', autospec=True)
    @mock.patch('azure.cli.command_modules.appservice.custom.parse_resource_id')
    @mock.patch('azure.cli.command_modules.appservice.custom.validate_zip_deploy_app_setting_exists')
    @mock.patch('azure.cli.command_modules.appservice.custom.upload_zip_to_storage')
    @mock.patch('azure.cli.command_modules.appservice.custom.is_plan_consumption', return_value=True)
    @mock.patch('azure.cli.command_modules.appservice.custom.is_flex_functionapp', return_value=False)
    def test_functionapp_linux_consumption_non_remote_build_with_slot(self,
                                                            is_flex_functionapp_mock,
                                                            is_plan_consumption_mock,
                                                            upload_zip_to_storage_mock,
                                                            validate_zip_deploy_app_setting_exists_mock,
                                                            parse_resource_id_mock,
                                                            web_client_factory_mock,
                                                            check_language_runtime_mock):
        # prepare
        cmd_mock = _get_test_cmd()
        cli_ctx_mock = mock.MagicMock()
        cmd_mock.cli_ctx = cli_ctx_mock

        appservice_mock = mock.Mock()
        appservice_mock.reserved = True  # Marked app service as Linux

        web_client_mock = mock.Mock()
        web_client_mock.web_apps = mock.Mock()
        web_client_mock.web_apps.get = mock.Mock(return_value=appservice_mock)
        web_client_factory_mock.return_value = web_client_mock

        # action
        # Linux Consumption app should use update-storage to deploy when not using remote build
        enable_zip_deploy_functionapp(cmd_mock, 'rg', 'name', 'src', build_remote=False, timeout=None, slot='slot')

        # assert
        web_client_mock.web_apps.get.assert_called_with('rg', 'name')
        upload_zip_to_storage_mock.assert_called_with(cmd_mock, 'rg', 'name', 'src', 'slot')

    @mock.patch('azure.cli.command_modules.appservice.custom.check_language_runtime')
    @mock.patch('azure.cli.command_modules.appservice.custom.add_remote_build_app_settings')
    @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory', autospec=True)
    @mock.patch('azure.cli.command_modules.appservice.custom.parse_resource_id')
    @mock.patch('azure.cli.command_modules.appservice.custom.is_flex_functionapp', return_value=False)
    @mock.patch('azure.cli.command_modules.appservice.custom.enable_zip_deploy')
    def test_functionapp_remote_build_supports_linux(self,
                                                     enable_zip_deploy_mock,
                                                     is_flex_functionapp_mock,
                                                     parse_resource_id_mock,
                                                     web_client_factory_mock,
                                                     add_remote_build_app_settings_mock,
                                                     check_language_runtime_mock):
        # prepare
        cmd_mock = _get_test_cmd()
        cli_ctx_mock = mock.MagicMock()
        cmd_mock.cli_ctx = cli_ctx_mock

        appservice_mock = mock.Mock()
        appservice_mock.reserved = True  # Marked app service as Linux

        web_client_mock = mock.Mock()
        web_client_mock.web_apps = mock.Mock()
        web_client_mock.web_apps.get = mock.Mock(return_value=appservice_mock)
        web_client_factory_mock.return_value = web_client_mock

        # action
        enable_zip_deploy_functionapp(cmd_mock, 'rg', 'name', 'src', build_remote=True, timeout=None, slot=None)

        # assert
        web_client_mock.web_apps.get.assert_called_with('rg', 'name')
        enable_zip_deploy_mock.assert_called_with(cmd_mock, 'rg', 'name', 'src', None, None)

    @mock.patch('azure.cli.command_modules.appservice.custom._get_scm_url', return_value='https://mock-scm')
    @mock.patch('azure.cli.command_modules.appservice.custom.get_runtime_config')
    @mock.patch('azure.cli.command_modules.appservice.custom.get_scm_site_headers_flex')
    @mock.patch('requests.post', autospec=True)
    @mock.patch('azure.cli.command_modules.appservice.custom._check_zip_deployment_status_flex')
    def test_enable_zip_deploy_flex(self,
                                    check_zip_deployment_status_mock,
                                    requests_post_mock,
                                    get_scm_site_headers_flex_mock,
                                    get_runtime_config_mock,
                                    get_scm_url_mock):
        # prepare
        cmd_mock = _get_test_cmd()
        cli_ctx_mock = mock.MagicMock()
        cmd_mock.cli_ctx = cli_ctx_mock

        response = mock.MagicMock()
        response.status_code = 202
        requests_post_mock.return_value = response

        get_runtime_config_mock.return_value = {
            "name": "java",
        }

        expected_zip_deploy_headers = _get_flex_zip_deploy_headers(cmd_mock.cli_ctx)
        get_scm_site_headers_flex_mock.return_value = expected_zip_deploy_headers

        # action
        with mock.patch('builtins.open', new_callable=mock.mock_open, read_data='zip-content'):
            enable_zip_deploy_flex(cmd_mock, 'rg', 'name', 'src', slot=None, build_remote=True)

        # assert
        requests_post_mock.assert_called_with('https://mock-scm/api/publish?RemoteBuild=True&Deployer=az_cli', data='zip-content',
                                              headers=expected_zip_deploy_headers, verify=mock.ANY)
        # TODO improve authorization matcher
        check_zip_deployment_status_mock.assert_called_with(cmd_mock, 'rg', 'name',
                                                            'https://mock-scm/api/deployments/latest', None)

    @mock.patch('time.sleep')
    @mock.patch('requests.get', autospec=True)
    @mock.patch('azure.cli.core.util.should_disable_connection_verify', return_value=False)
    @mock.patch('azure.cli.command_modules.appservice.custom.list_host_keys')
    @mock.patch('azure.cli.command_modules.appservice.custom._get_host_url',
                return_value='https://mock-func.azurewebsites.net')
    def test_check_flex_app_after_deployment_success(self,
                                                     get_host_url_mock,
                                                     list_host_keys_mock,
                                                     should_disable_verify_mock,
                                                     requests_get_mock,
                                                     sleep_mock):
        # prepare
        cmd_mock = _get_test_cmd()
        list_host_keys_mock.return_value = mock.Mock(master_key='master-key')
        response = mock.Mock(status_code=200, reason='OK')
        requests_get_mock.return_value = response

        # action
        result = check_flex_app_after_deployment(cmd_mock, 'rg', 'name')

        # assert
        self.assertEqual(result, "Deployment was successful.")
        requests_get_mock.assert_called_with('https://mock-func.azurewebsites.net/admin/host/status',
                                             headers={"x-functions-key": 'master-key'},
                                             verify=True)

    @mock.patch('time.sleep')
    @mock.patch('azure.cli.command_modules.appservice.custom._get_host_url', side_effect=ValueError())
    def test_check_flex_app_after_deployment_host_url_fetch_failure(self,
                                                                    get_host_url_mock,
                                                                    sleep_mock):
        # prepare
        cmd_mock = _get_test_cmd()

        # action
        with self.assertRaises(ResourceNotFoundError):
            check_flex_app_after_deployment(cmd_mock, 'rg', 'name')

        # assert
        get_host_url_mock.assert_called_once_with(cmd_mock, 'rg', 'name')

    @mock.patch('time.sleep')
    @mock.patch('azure.cli.command_modules.appservice.custom.list_host_keys', side_effect=Exception())
    @mock.patch('azure.cli.command_modules.appservice.custom._get_host_url',
                return_value='https://mock-func.azurewebsites.net')
    def test_check_flex_app_after_deployment_host_key_fetch_failure(self,
                                                                    get_host_url_mock,
                                                                    list_host_keys_mock,
                                                                    sleep_mock):
        # prepare
        cmd_mock = _get_test_cmd()

        # action
        with self.assertRaises(ResourceNotFoundError):
            check_flex_app_after_deployment(cmd_mock, 'rg', 'name')

        # assert
        list_host_keys_mock.assert_called_once_with(cmd_mock, 'rg', 'name')

    @mock.patch('time.sleep')
    @mock.patch('requests.get', autospec=True)
    @mock.patch('azure.cli.core.util.should_disable_connection_verify', return_value=False)
    @mock.patch('azure.cli.command_modules.appservice.custom.list_host_keys')
    @mock.patch('azure.cli.command_modules.appservice.custom._get_host_url',
                return_value='https://mock-func.azurewebsites.net')
    def test_check_flex_app_after_deployment_ip_restriction(self,
                                                            get_host_url_mock,
                                                            list_host_keys_mock,
                                                            should_disable_verify_mock,
                                                            requests_get_mock,
                                                            sleep_mock):
        # prepare
        cmd_mock = _get_test_cmd()
        list_host_keys_mock.return_value = mock.Mock(master_key='master-key')
        requests_get_mock.return_value = mock.Mock(status_code=403, reason='Ip Forbidden')

        # action
        result = check_flex_app_after_deployment(cmd_mock, 'rg', 'name')

        # assert
        self.assertEqual(result, "Deployment was successful but health check failed due to IP restriction.")

    @mock.patch('time.sleep')
    @mock.patch('requests.get', autospec=True)
    @mock.patch('azure.cli.core.util.should_disable_connection_verify', return_value=False)
    @mock.patch('azure.cli.command_modules.appservice.custom.list_host_keys')
    @mock.patch('azure.cli.command_modules.appservice.custom._get_host_url',
                return_value='https://mock-func.azurewebsites.net')
    def test_check_flex_app_after_deployment_unhealthy(self,
                                                       get_host_url_mock,
                                                       list_host_keys_mock,
                                                       should_disable_verify_mock,
                                                       requests_get_mock,
                                                       sleep_mock):
        # prepare
        cmd_mock = _get_test_cmd()
        list_host_keys_mock.return_value = mock.Mock(master_key='master-key')
        requests_get_mock.return_value = mock.Mock(status_code=500, reason='Internal Server Error')

        # action
        with self.assertRaises(CLIError):
            check_flex_app_after_deployment(cmd_mock, 'rg', 'name')

        # assert
        self.assertEqual(requests_get_mock.call_count, 15)


    @mock.patch('azure.cli.command_modules.appservice.custom.get_scm_site_headers')
    @mock.patch('azure.cli.command_modules.appservice.custom._get_scm_url', side_effect=ValueError())
    def test_enable_zip_deploy_remote_build_no_scm_site(self,
                                                        get_scm_url_mock,
                                                        get_scm_headers_mock):
        # prepare
        cmd_mock = _get_test_cmd()
        cli_ctx_mock = mock.MagicMock()
        cmd_mock.cli_ctx = cli_ctx_mock

        # action
        # When the function app is created before 8/1/2019, it cannot use remote build
        with self.assertRaises(CLIError):
            enable_zip_deploy(cmd_mock, 'rg', 'name', 'src', slot=None)

        # assert
        get_scm_url_mock.assert_called_with(cmd_mock, 'rg', 'name', None)

    @mock.patch('azure.cli.command_modules.appservice.custom.get_scm_site_headers')
    @mock.patch('azure.cli.command_modules.appservice.custom._get_scm_url', return_value='https://mock-scm')
    @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory', autospec=True)
    @mock.patch('requests.post', autospec=True)
    @mock.patch('azure.cli.command_modules.appservice.custom._check_zip_deployment_status')
    def test_enable_zip_deploy_accepted(self,
                                        check_zip_deployment_status_mock,
                                        requests_post_mock,
                                        web_client_factory_mock,
                                        get_scm_url_mock,
                                        get_scm_headers_mock):
        # prepare
        cmd_mock = _get_test_cmd()
        cli_ctx_mock = mock.MagicMock()
        cmd_mock.cli_ctx = cli_ctx_mock

        response = mock.MagicMock()
        response.status_code = 202
        requests_post_mock.return_value = response

        appservice_mock = mock.Mock()
        appservice_mock.kind = "functionapp"

        web_client_mock = mock.Mock()
        web_client_mock.web_apps = mock.Mock()
        web_client_mock.web_apps.get = mock.Mock(return_value=appservice_mock)
        web_client_factory_mock.return_value = web_client_mock

        expected_zip_deploy_headers = _get_zip_deploy_headers('usr', 'pwd', cmd_mock.cli_ctx)
        get_scm_headers_mock.return_value = expected_zip_deploy_headers

        # action
        with mock.patch('builtins.open', new_callable=mock.mock_open, read_data='zip-content'):
            enable_zip_deploy(cmd_mock, 'rg', 'name', 'src', slot=None)

        # assert
        requests_post_mock.assert_called_with('https://mock-scm/api/zipdeploy?isAsync=true&Deployer=az_cli_functions', data='zip-content',
                                              headers=expected_zip_deploy_headers, verify=mock.ANY)
        # TODO improve authorization matcher
        check_zip_deployment_status_mock.assert_called_with(cmd_mock, 'rg', 'name',
                                                            'https://mock-scm/api/deployments/latest', mock.ANY, None)

    @mock.patch('azure.cli.command_modules.appservice.custom.get_scm_site_headers')
    @mock.patch('azure.cli.command_modules.appservice.custom._get_scm_url', return_value='https://mock-scm')
    @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory', autospec=True)
    @mock.patch('requests.post', autospec=True)
    def test_enable_zip_deploy_conflict(self,
                                        requests_post_mock,
                                        web_client_factory_mock,
                                        get_scm_url_mock,
                                        get_scm_headers_mock):
        # prepare
        cmd_mock = _get_test_cmd()
        cli_ctx_mock = mock.MagicMock()
        cmd_mock.cli_ctx = cli_ctx_mock

        response = mock.MagicMock()
        response.status_code = 409
        requests_post_mock.return_value = response

        appservice_mock = mock.Mock()
        appservice_mock.kind = "functionapp"

        web_client_mock = mock.Mock()
        web_client_mock.web_apps = mock.Mock()
        web_client_mock.web_apps.get = mock.Mock(return_value=appservice_mock)
        web_client_factory_mock.return_value = web_client_mock

        expected_zip_deploy_headers = _get_zip_deploy_headers('usr', 'pwd', cmd_mock.cli_ctx)
        get_scm_headers_mock.return_value = expected_zip_deploy_headers

        # action
        with mock.patch('builtins.open', new_callable=mock.mock_open, read_data='zip-content'):
            with self.assertRaises(UnclassifiedUserFault):
                enable_zip_deploy(cmd_mock, 'rg', 'name', 'src', slot=None)

        # assert
        requests_post_mock.assert_called_with('https://mock-scm/api/zipdeploy?isAsync=true&Deployer=az_cli_functions', data='zip-content',
                                              headers=expected_zip_deploy_headers, verify=mock.ANY)

    @mock.patch('azure.cli.command_modules.appservice.custom.get_scm_site_headers')
    @mock.patch('azure.cli.command_modules.appservice.custom._get_scm_url', return_value='https://mock-scm')
    @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory', autospec=True)
    @mock.patch('requests.post', autospec=True)
    def test_enable_zip_deploy_service_unavailable(self,
                                                   requests_post_mock,
                                                   web_client_factory_mock,
                                                   get_scm_url_mock,
                                                   get_scm_headers_mock):
        # prepare
        cmd_mock = _get_test_cmd()
        cli_ctx_mock = mock.MagicMock()
        cmd_mock.cli_ctx = cli_ctx_mock

        response = mock.MagicMock()
        response.status_code = 503
        requests_post_mock.return_value = response

        appservice_mock = mock.Mock()
        appservice_mock.kind = "functionapp"

        web_client_mock = mock.Mock()
        web_client_mock.web_apps = mock.Mock()
        web_client_mock.web_apps.get = mock.Mock(return_value=appservice_mock)
        web_client_factory_mock.return_value = web_client_mock


        expected_zip_deploy_headers = _get_zip_deploy_headers('usr', 'pwd', cmd_mock.cli_ctx)
        get_scm_headers_mock.return_value = expected_zip_deploy_headers

        # action
        with mock.patch('builtins.open', new_callable=mock.mock_open, read_data='zip-content'):
            with self.assertRaises(AzureInternalError):
                enable_zip_deploy(cmd_mock, 'rg', 'name', 'src', slot=None)

        # assert
        requests_post_mock.assert_called_with('https://mock-scm/api/zipdeploy?isAsync=true&Deployer=az_cli_functions', data='zip-content',
                                              headers=expected_zip_deploy_headers, verify=mock.ANY)

    @mock.patch('azure.cli.command_modules.appservice.custom._get_app_settings_from_scm', return_value={
        'SCM_DO_BUILD_DURING_DEPLOYMENT': 'true'
    })
    def test_validate_app_settings_in_scm_should_have(self, get_app_settings_from_scm_mock):
        # prepare
        cmd_mock = _get_test_cmd()
        should_have = ['SCM_DO_BUILD_DURING_DEPLOYMENT']

        # action
        result = validate_app_settings_in_scm(cmd_mock, 'rg', 'name', slot=None, should_have=should_have)

        # assert
        self.assertTrue(result)

    @mock.patch('azure.cli.command_modules.appservice.custom._get_app_settings_from_scm', return_value={
        'SCM_DO_BUILD_DURING_DEPLOYMENT': 'true'
    })
    def test_validate_app_settings_in_scm_should_not_have(self, get_app_settings_from_scm_mock):
        # prepare
        cmd_mock = _get_test_cmd()
        should_not_have = ['ENABLE_ORYX_BUILD']

        # action
        result = validate_app_settings_in_scm(cmd_mock, 'rg', 'name', slot=None, should_not_have=should_not_have)

        # assert
        self.assertTrue(result)

    @mock.patch('azure.cli.command_modules.appservice.custom._get_app_settings_from_scm', return_value={
        'SCM_DO_BUILD_DURING_DEPLOYMENT': 'true'
    })
    def test_validate_app_settings_in_scm_should_contain(self, get_app_settings_from_scm_mock):
        # prepare
        cmd_mock = _get_test_cmd()
        should_contain = {'SCM_DO_BUILD_DURING_DEPLOYMENT': 'true'}

        # action
        result = validate_app_settings_in_scm(cmd_mock, 'rg', 'name', slot=None, should_contain=should_contain)

        # assert
        self.assertTrue(result)

    @mock.patch('azure.cli.command_modules.appservice.custom._get_app_settings_from_scm', return_value={
        'SCM_DO_BUILD_DURING_DEPLOYMENT': 'true'
    })
    def test_validate_app_settings_in_scm_should_have_failure(self, get_app_settings_from_scm_mock):
        # prepare
        cmd_mock = _get_test_cmd()
        should_have = ['ENABLE_ORYX_BUILD']

        # action
        result = validate_app_settings_in_scm(cmd_mock, 'rg', 'name', slot=None, should_have=should_have)

        # assert
        self.assertFalse(result)

    @mock.patch('azure.cli.command_modules.appservice.custom._get_app_settings_from_scm', return_value={
        'SCM_DO_BUILD_DURING_DEPLOYMENT': 'true'
    })
    def test_validate_app_Settings_in_scm_should_not_have_failure(self, get_app_settings_from_scm_mock):
        # prepare
        cmd_mock = _get_test_cmd()
        should_not_have = ['SCM_DO_BUILD_DURING_DEPLOYMENT']

        # action
        result = validate_app_settings_in_scm(cmd_mock, 'rg', 'name', slot=None, should_not_have=should_not_have)

        # assert
        self.assertFalse(result)

    @mock.patch('azure.cli.command_modules.appservice.custom._get_app_settings_from_scm', return_value={
        'SCM_DO_BUILD_DURING_DEPLOYMENT': 'true'
    })
    def test_validate_app_settings_in_scm_should_contain_failure(self, get_app_settings_from_scm_mock):
        # prepare
        cmd_mock = _get_test_cmd()
        should_contain = {'SCM_DO_BUILD_DURING_DEPLOYMENT': 'false'}

        # action
        result = validate_app_settings_in_scm(cmd_mock, 'rg', 'name', slot=None, should_contain=should_contain)

        # assert
        self.assertFalse(result)

    @mock.patch('azure.cli.command_modules.appservice.custom.check_language_runtime')
    @mock.patch('azure.cli.command_modules.appservice.custom.is_centauri_functionapp')
    @mock.patch('azure.cli.command_modules.appservice.custom._generic_site_operation')
    @mock.patch('azure.cli.command_modules.appservice.custom.update_functionapp_polling', return_value=True)
    @mock.patch('azure.cli.command_modules.appservice.custom.update_container_settings', autospec=True)
    def test_update_container_settings_functionapp(self,
                                                   update_container_settings_mock,
                                                   update_functionapp_polling_mock,
                                                   site_op_mock,
                                                   is_centauri_functionapp_mock,
                                                   check_language_runtime_mock):
        # prepare
        cmd_mock = _get_test_cmd()
        cli_ctx_mock = mock.MagicMock()
        cmd_mock.cli_ctx = cli_ctx_mock

        Site, DaprConfig, ResourceConfig = cmd_mock.get_models('Site', 'DaprConfig', 'ResourceConfig')
        site = Site(dapr_config=None, location='westus', name='name', resource_config=ResourceConfig())
        site_op_mock.return_value = site

        is_centauri_functionapp_mock.return_value = True

        check_language_runtime_mock.return_value = True

        # action
        update_container_settings_functionapp(cmd_mock, 'rg', 'name', workload_profile_name='d4', cpu=0.5, memory='1Gi')

        # assert
        updated_site = site
        updated_site.dapr_config = DaprConfig()
        update_functionapp_polling_mock.assert_called_with(cmd_mock, 'rg', 'name', updated_site)


    @mock.patch('azure.cli.command_modules.appservice.custom.validate_app_settings_in_scm', return_value=True)
    @mock.patch('azure.cli.command_modules.appservice.custom.update_app_settings')
    @mock.patch('azure.cli.command_modules.appservice.custom.delete_app_settings')
    @mock.patch('azure.cli.command_modules.appservice.custom.get_app_settings', return_value=[])
    def test_add_remote_build_app_settings_add_scm_do_build_during_deployment(self,
                                                                              get_app_settings_mock,
                                                                              delete_app_settings_mock,
                                                                              update_app_settings_mock,
                                                                              validate_app_settings_in_scm_mock):
        # prepare
        cmd_mock = _get_test_cmd()

        # action
        add_remote_build_app_settings(cmd_mock, 'rg', 'name', slot=None)

        # assert
        update_app_settings_mock.assert_called_with(cmd_mock, 'rg', 'name', ['SCM_DO_BUILD_DURING_DEPLOYMENT=true'], None)
        validate_app_settings_in_scm_mock.assert_called_with(cmd_mock, 'rg', 'name', None,
                                                             should_contain={'SCM_DO_BUILD_DURING_DEPLOYMENT': 'true'},
                                                             should_not_have=[])

    @mock.patch('azure.cli.command_modules.appservice.custom.validate_app_settings_in_scm',
                return_value=True)
    @mock.patch('azure.cli.command_modules.appservice.custom.update_app_settings')
    @mock.patch('azure.cli.command_modules.appservice.custom.delete_app_settings')
    @mock.patch('azure.cli.command_modules.appservice.custom.get_app_settings', return_value=[
        {
            'name': 'WEBSITE_RUN_FROM_PACKAGE',
            'value': 'https://microsoft.com'
        },
        {
            'name': 'ENABLE_ORYX_BUILD',
            'value': 'true'
        }
    ])
    def test_add_remote_build_app_settings_remove_unnecessary_app_settings(self,
                                                                           get_app_settings_mock,
                                                                           delete_app_settings_mock,
                                                                           update_app_settings_mock,
                                                                           validate_app_settings_in_scm_mock):
        # prepare
        cmd_mock = _get_test_cmd()

        # action
        add_remote_build_app_settings(cmd_mock, 'rg', 'name', slot=None)

        # assert
        delete_app_settings_mock.assert_any_call(cmd_mock, 'rg', 'name', ['WEBSITE_RUN_FROM_PACKAGE'], None)
        delete_app_settings_mock.assert_any_call(cmd_mock, 'rg', 'name', ['ENABLE_ORYX_BUILD'], None)
        validate_app_settings_in_scm_mock.assert_called_with(cmd_mock, 'rg', 'name', None,
                                                             should_contain={'SCM_DO_BUILD_DURING_DEPLOYMENT': 'true'},
                                                             should_not_have=['WEBSITE_RUN_FROM_PACKAGE', 'ENABLE_ORYX_BUILD'])

    @mock.patch('azure.cli.command_modules.appservice.custom.validate_app_settings_in_scm', return_value=True)
    @mock.patch('azure.cli.command_modules.appservice.custom.update_app_settings')
    @mock.patch('azure.cli.command_modules.appservice.custom.delete_app_settings')
    @mock.patch('azure.cli.command_modules.appservice.custom.get_app_settings', return_value=[{
        'name': 'SCM_DO_BUILD_DURING_DEPLOYMENT',
        'value': 'false'
    }])
    def test_add_remote_build_app_settings_change_scm_do_build_during_deployment(self,
                                                                                 get_app_settings_mock,
                                                                                 delete_app_settings_mock,
                                                                                 update_app_settings_mock,
                                                                                 validate_app_settings_in_scm_mock):
        # prepare
        cmd_mock = _get_test_cmd()

        # action
        add_remote_build_app_settings(cmd_mock, 'rg', 'name', slot=None)

        # assert
        update_app_settings_mock.assert_called_with(cmd_mock, 'rg', 'name', ['SCM_DO_BUILD_DURING_DEPLOYMENT=true'], None)
        validate_app_settings_in_scm_mock.assert_called_with(cmd_mock, 'rg', 'name', None,
                                                             should_contain={'SCM_DO_BUILD_DURING_DEPLOYMENT': 'true'},
                                                             should_not_have=[])

    @mock.patch('azure.cli.command_modules.appservice.custom.validate_app_settings_in_scm', return_value=True)
    @mock.patch('azure.cli.command_modules.appservice.custom.update_app_settings')
    @mock.patch('azure.cli.command_modules.appservice.custom.delete_app_settings')
    @mock.patch('azure.cli.command_modules.appservice.custom.get_app_settings', return_value=[{
        'name': 'SCM_DO_BUILD_DURING_DEPLOYMENT',
        'value': 'true'
    }])
    def test_add_remote_build_app_settings_do_nothing(self,
                                                      get_app_settings_mock,
                                                      delete_app_settings_mock,
                                                      update_app_settings_mock,
                                                      validate_app_settings_in_scm_mock):
        # prepare
        cmd_mock = _get_test_cmd()

        # action
        add_remote_build_app_settings(cmd_mock, 'rg', 'name', slot=None)

        # assert
        update_app_settings_mock.assert_not_called()
        validate_app_settings_in_scm_mock.assert_not_called()

    @mock.patch('azure.cli.command_modules.appservice.custom.validate_app_settings_in_scm', return_value=True)
    @mock.patch('azure.cli.command_modules.appservice.custom.update_app_settings')
    @mock.patch('azure.cli.command_modules.appservice.custom.delete_app_settings')
    @mock.patch('azure.cli.command_modules.appservice.custom.get_app_settings', return_value=[])
    def test_remove_remote_build_app_settings_disable_scm_do_build_during_deployment(self,
                                                                                     get_app_settings_mock,
                                                                                     delete_app_settings_mock,
                                                                                     update_app_settings_mock,
                                                                                     validate_app_settings_in_scm_mock):
        # prepare
        cmd_mock = _get_test_cmd()

        # action
        remove_remote_build_app_settings(cmd_mock, 'rg', 'name', slot=None)

        # assert
        update_app_settings_mock.assert_called_with(cmd_mock, 'rg', 'name', ['SCM_DO_BUILD_DURING_DEPLOYMENT=false'], None)
        validate_app_settings_in_scm_mock.assert_called_with(cmd_mock, 'rg', 'name', None,
                                                             should_contain={'SCM_DO_BUILD_DURING_DEPLOYMENT': 'false'})

    @mock.patch('azure.cli.command_modules.appservice.custom.validate_app_settings_in_scm', return_value=True)
    @mock.patch('azure.cli.command_modules.appservice.custom.update_app_settings')
    @mock.patch('azure.cli.command_modules.appservice.custom.delete_app_settings')
    @mock.patch('azure.cli.command_modules.appservice.custom.get_app_settings', return_value=[{
        'name': 'SCM_DO_BUILD_DURING_DEPLOYMENT',
        'value': 'false'
    }])
    def test_remove_remote_build_app_settings_do_nothing(self,
                                                         get_app_settings_mock,
                                                         delete_app_settings_mock,
                                                         update_app_settings_mock,
                                                         validate_app_settings_in_scm_mock):
        # prepare
        cmd_mock = _get_test_cmd()

        # action
        remove_remote_build_app_settings(cmd_mock, 'rg', 'name', slot=None)

        # assert
        update_app_settings_mock.assert_not_called()
        validate_app_settings_in_scm_mock.assert_not_called()


    @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory', autospec=True)
    @mock.patch('azure.cli.command_modules.appservice.utils._get_location_from_webapp')
    @mock.patch('azure.cli.command_modules.appservice.custom._generic_site_operation', autospec=True)
    @mock.patch('azure.cli.command_modules.appservice.custom.LongRunningOperation.__call__', autospec=True)
    def test_config_source_control(self,
                                   long_running_operation_mock,
                                   site_op_mock,
                                   location_mock,
                                   web_client_factory_mock):
        # prepare
        client = mock.Mock()
        web_client_factory_mock.return_value = client

        location_mock.return_value = mock.MagicMock()

        site_op_mock.return_value = mock.MagicMock()

        cmd_mock = _get_test_cmd()

        SiteSourceControl, GitHubActionConfiguration, GitHubActionContainerConfiguration = cmd_mock.get_models('SiteSourceControl', 'GitHubActionConfiguration', 'GitHubActionContainerConfiguration')
        container_config = GitHubActionContainerConfiguration(username="username", password="password")
        github_action_config = GitHubActionConfiguration(container_configuration=container_config)
        source_control = SiteSourceControl(git_hub_action_configuration=github_action_config)

        long_running_operation_mock.return_value = source_control

        # action
        response = config_source_control(cmd_mock, 'rg', 'functionapp', 'https://github.com/yugang/azure-site-test')

        # assert
        self.assertEqual(response.git_hub_action_configuration.container_configuration.password, None)

    def test_flex_parse_raw_stacks_handles_empty_app_settings_dictionary(self):
        from azure.cli.command_modules.appservice.custom import _FlexFunctionAppStackRuntimeHelper

        # prepare
        cmd_mock = _get_test_cmd()
        go_stack = {
            'name': 'go',
            'properties': {'majorVersions': [{'minorVersions': [{
                'value': '1.0',
                'stackSettings': {'linuxRuntimeSettings': {
                    'appSettingsDictionary': {},
                    'Sku': [{'skuCode': 'FC1'}],
                    'gitHubActionSettings': {'isSupported': True},
                    'appInsightsSettings': {'isSupported': True},
                    'isDefault': True,
                }},
            }]}]},
        }

        # action
        with mock.patch.object(_FlexFunctionAppStackRuntimeHelper,
                               'get_flex_raw_function_app_stacks',
                               return_value=[go_stack]):
            matched = _FlexFunctionAppStackRuntimeHelper(cmd_mock, 'westcentralus', 'go').resolve('go', '1.0')

        # assert
        self.assertEqual(matched.name, 'go')
        self.assertEqual(matched.version, '1.0')

    def test_flex_parse_raw_stacks_prefers_functions_worker_runtime_when_present(self):
        # FUNCTIONS_WORKER_RUNTIME overrides runtime['name'] when set (e.g., dotnet-isolated under the dotnet stack).
        from azure.cli.command_modules.appservice.custom import _FlexFunctionAppStackRuntimeHelper

        # prepare
        cmd_mock = _get_test_cmd()
        dotnet_isolated_stack = {
            'name': 'dotnet',
            'properties': {'majorVersions': [{'minorVersions': [{
                'value': '8.0',
                'stackSettings': {'linuxRuntimeSettings': {
                    'appSettingsDictionary': {'FUNCTIONS_WORKER_RUNTIME': 'dotnet-isolated'},
                    'Sku': [{'skuCode': 'FC1'}],
                    'gitHubActionSettings': {'isSupported': True},
                    'appInsightsSettings': {'isSupported': True},
                    'isDefault': True,
                }},
            }]}]},
        }

        # action
        with mock.patch.object(_FlexFunctionAppStackRuntimeHelper,
                               'get_flex_raw_function_app_stacks',
                               return_value=[dotnet_isolated_stack]):
            matched = _FlexFunctionAppStackRuntimeHelper(
                cmd_mock, 'westcentralus', 'dotnet-isolated').resolve('dotnet-isolated', '8.0')

        # assert
        self.assertEqual(matched.name, 'dotnet-isolated')

    @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory', autospec=True)
    def test_list_function_keys_unwraps_broken_string_dictionary(self, web_client_factory_mock):
        # azure-mgmt-web 11.0.0 deserializes flat dictionary responses with .properties = None
        from azure.mgmt.web import models as _models
        from azure.mgmt.web._utils.model_base import _deserialize

        broken = _deserialize(
            _models.StringDictionary,
            {'default': 'vvrX4LJY1JWbimFI28UM', 'myCustomKey': 'abc'})
        self.assertIsNone(broken.properties)

        cmd_mock = _get_test_cmd()
        client_mock = mock.MagicMock()
        client_mock.web_apps.list_function_keys.return_value = broken
        web_client_factory_mock.return_value = client_mock

        result = list_function_keys(cmd_mock, 'rg', 'app', 'httpget')

        self.assertEqual(result, {'default': 'vvrX4LJY1JWbimFI28UM', 'myCustomKey': 'abc'})
        client_mock.web_apps.list_function_keys.assert_called_once_with('rg', 'app', 'httpget')
        client_mock.web_apps.list_function_keys_slot.assert_not_called()

    @mock.patch('azure.cli.command_modules.appservice.custom.web_client_factory', autospec=True)
    def test_list_function_keys_uses_properties_when_sdk_returns_enveloped_response(self, web_client_factory_mock):
        # Prefers .properties when populated (forward-compatible with a future SDK fix)
        fixed = mock.MagicMock()
        fixed.properties = {'default': 'abc'}

        cmd_mock = _get_test_cmd()
        client_mock = mock.MagicMock()
        client_mock.web_apps.list_function_keys_slot.return_value = fixed
        web_client_factory_mock.return_value = client_mock

        result = list_function_keys(cmd_mock, 'rg', 'app', 'httpget', slot='staging')

        self.assertEqual(result, {'default': 'abc'})
        client_mock.web_apps.list_function_keys_slot.assert_called_once_with('rg', 'app', 'httpget', 'staging')
        client_mock.web_apps.list_function_keys.assert_not_called()
