text: # coding: utf-8
  #
  # Copyright 2018 The Oppia Authors. All Rights Reserved.
  #
  # Licensed under the Apache License, Version 2.0 (the "License");
  # you may not use this file except in compliance with the License.
  # You may obtain a copy of the License at
  #
  #      http://www.apache.org/licenses/LICENSE-2.0
  #
  # Unless required by applicable law or agreed to in writing, software
  # distributed under the License is distributed on an "AS-IS" BASIS,
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  # See the License for the specific language governing permissions and
  # limitations under the License.
  
  """Tests for state domain objects and methods defined on them."""
  
  from __future__ import absolute_import
  from __future__ import unicode_literals
  
  import copy
  import logging
  import os
  import re
  
  from core.domain import exp_domain
  from core.domain import exp_fetchers
  from core.domain import exp_services
  from core.domain import html_validation_service
  from core.domain import interaction_registry
  from core.domain import rules_registry
  from core.domain import state_domain
  from core.domain import translatable_object_registry
  from core.tests import test_utils
  import feconf
  import python_utils
  import schema_utils
  import utils
  
  
  class StateDomainUnitTests(test_utils.GenericTestBase):
      """Test methods operating on states."""
  
      def test_get_all_html_in_exploration_with_drag_and_drop_interaction(self):
          """Test the method for extracting all the HTML from a state having
          DragAndDropSortInput interaction.
          """
          exploration = exp_domain.Exploration.create_default_exploration(
              'exp_id')
          exploration.add_states(['State1'])
          state = exploration.states['State1']
          state_content_dict = {
              'content_id': 'content',
              'html': '<p>state content html</p>'
          }
          state_customization_args_dict = {
              'choices': {
                  'value': [
                      {
                          'content_id': 'ca_choices_0',
                          'html': '<p>state customization arg html 1</p>'
                      }, {
                          'content_id': 'ca_choices_1',
                          'html': '<p>state customization arg html 2</p>'
                      }, {
                          'content_id': 'ca_choices_2',
                          'html': '<p>state customization arg html 3</p>'
                      }, {
                          'content_id': 'ca_choices_3',
                          'html': '<p>state customization arg html 4</p>'
                      }
                  ]
              },
              'allowMultipleItemsInSamePosition': {
                  'value': False
              }
          }
          state_answer_group = state_domain.AnswerGroup(
              state_domain.Outcome(
                  'Introduction', state_domain.SubtitledHtml(
                      'feedback_1', '<p>State Feedback</p>'),
                  False, [], None, None),
              [
                  state_domain.RuleSpec(
                      'IsEqualToOrdering',
                      {
                          'x': [['<p>IsEqualToOrdering rule_spec htmls</p>']]
                      }),
                  state_domain.RuleSpec(
                      'HasElementXAtPositionY',
                      {
                          'x': '<p>HasElementXAtPositionY rule_spec '
                               'html</p>',
                          'y': 2
                      }),
                  state_domain.RuleSpec(
                      'HasElementXBeforeElementY',
                      {
                          'x': '<p>x input for HasElementXAtPositionY '
                               'rule_spec </p>',
                          'y': '<p>y input for HasElementXAtPositionY '
                               'rule_spec </p>'
                      }),
                  state_domain.RuleSpec(
                      'IsEqualToOrderingWithOneItemAtIncorrectPosition',
                      {
                          'x': [[(
                              '<p>IsEqualToOrderingWithOneItemAtIncorrectPos'
                              'ition rule_spec htmls</p>')
                                ]]
                      })
              ],
              [],
              None
          )
          state_solution_dict = {
              'answer_is_exclusive': True,
              'correct_answer': [
                  '<p>state customization arg html 1</p>',
                  '<p>state customization arg html 2</p>',
                  '<p>state customization arg html 3</p>',
                  '<p>state customization arg html 4</p>'
              ],
              'explanation': {
                  'content_id': 'solution',
                  'html': '<p>This is solution for state1</p>'
              }
          }
          state_written_translations_dict = {
              'translations_mapping': {
                  'content': {
                      'en': {
                          'data_format': 'html',
                          'translation':
                              '<p>state written_translation content-en</p>',
                          'needs_update': True
                      },
                      'hi': {
                          'data_format': 'html',
                          'translation':
                              '<p>state written_translation content-hi</p>',
                          'needs_update': False
                      }
                  },
                  'ca_choices_0': {
                      'hi': {
                          'data_format': 'html',
                          'translation':
                              (
                                  '<p>state written_translation ca_choices_0-hi'
                                  '</p>'
                              ),
                          'needs_update': False
                      },
                      'en': {
                          'data_format': 'html',
                          'translation':
                              (
                                  '<p>state written_translation ca_choices_0'
                                  '-en</p>'
                              ),
                          'needs_update': False
                      }
                  },
                  'ca_choices_1': {
                      'hi': {
                          'data_format': 'html',
                          'translation':
                              (
                                  '<p>state written_translation ca_choices_1-hi'
                                  '</p>'
                              ),
                          'needs_update': False
                      },
                      'en': {
                          'data_format': 'html',
                          'translation':
                              (
                                  '<p>state written_translation ca_choices_1-en'
                                  '</p>'
                              ),
                          'needs_update': False
                      }
                  },
                  'ca_choices_2': {
                      'hi': {
                          'data_format': 'html',
                          'translation':
                              (
                                  '<p>state written_translation ca_choices_2-hi'
                                  '</p>'
                              ),
                          'needs_update': False
                      },
                      'en': {
                          'data_format': 'html',
                          'translation':
                              (
                                  '<p>state written_translation ca_choices_2-en'
                                  '</p>'
                              ),
                          'needs_update': False
                      }
                  },
                  'ca_choices_3': {
                      'hi': {
                          'data_format': 'html',
                          'translation': (
                              '<p>state written_translation ca_choices_3-hi'
                              '</p>'
                          ),
                          'needs_update': False
                      },
                      'en': {
                          'data_format': 'html',
                          'translation':
                              (
                                  '<p>state written_translation ca_choices_3-en'
                                  '</p>'
                              ),
                          'needs_update': False
                      }
                  },
                  'default_outcome': {
                      'hi': {
                          'data_format': 'html',
                          'translation':
                              '<p>state written_translation outcome-hi</p>',
                          'needs_update': False
                      },
                      'en': {
                          'data_format': 'html',
                          'translation':
                              '<p>state written_translation outcome-en</p>',
                          'needs_update': False
                      }
                  },
                  'feedback_1': {
                      'hi': {
                          'data_format': 'html',
                          'translation':
                              '<p>state written_translation feedback-hi</p>',
                          'needs_update': False
                      },
                      'en': {
                          'data_format': 'html',
                          'translation':
                              '<p>state written_translation feedback-en</p>',
                          'needs_update': False
                      }
                  },
                  'hint_1': {
                      'hi': {
                          'data_format': 'html',
                          'translation':
                              '<p>state written_translation hint_1-hi</p>',
                          'needs_update': False
                      },
                      'en': {
                          'data_format': 'html',
                          'translation':
                              '<p>state written_translation hint_1-en</p>',
                          'needs_update': False
                      }
                  },
                  'solution': {
                      'hi': {
                          'data_format': 'html',
                          'translation':
                              '<p>state written_translation solution-hi</p>',
                          'needs_update': False
                      },
                      'en': {
                          'data_format': 'html',
                          'translation':
                              '<p>state written_translation solution-en</p>',
                          'needs_update': False
                      }
                  }
              }
          }
          state_hint_list = [
              state_domain.Hint(
                  state_domain.SubtitledHtml(
                      'hint_1', '<p>Hello, this is html1 for hint 1</p>'
                  )
              )
          ]
  
          state_solution_dict = {
              'answer_is_exclusive': True,
              'correct_answer': [
                  ['<p>state customization arg html 1</p>'],
                  ['<p>state customization arg html 2</p>'],
                  ['<p>state customization arg html 3</p>'],
                  ['<p>state customization arg html 4</p>']
              ],
              'explanation': {
                  'content_id': 'solution',
                  'html': '<p>This is solution for state1</p>'
              }
          }
  
          state.update_content(
              state_domain.SubtitledHtml.from_dict(state_content_dict))
          state.update_interaction_id('DragAndDropSortInput')
          state.update_interaction_customization_args(
              state_customization_args_dict)
          state.update_next_content_id_index(4)
          state.update_interaction_hints(state_hint_list)
  
          solution = state_domain.Solution.from_dict(
              state.interaction.id, state_solution_dict)
          state.update_interaction_solution(solution)
          state.update_interaction_answer_groups(
              [state_answer_group])
          state.update_written_translations(
              state_domain.WrittenTranslations.from_dict(
                  state_written_translations_dict))
  
          exp_services.save_new_exploration('owner_id', exploration)
  
          mock_html_field_types_to_rule_specs_dict = copy.deepcopy(
              rules_registry.Registry.get_html_field_types_to_rule_specs(
                  state_schema_version=41))
  
          def mock_get_html_field_types_to_rule_specs(unused_cls):
              return mock_html_field_types_to_rule_specs_dict
  
          def mock_get_interaction_by_id(cls, interaction_id):
              interaction = copy.deepcopy(cls._interactions[interaction_id]) # pylint: disable=protected-access
              interaction.answer_type = 'ListOfSetsOfHtmlStrings'
              return interaction
  
          rules_registry_swap = self.swap(
              rules_registry.Registry, 'get_html_field_types_to_rule_specs',
              classmethod(mock_get_html_field_types_to_rule_specs))
  
          interaction_registry_swap = self.swap(
              interaction_registry.Registry, 'get_interaction_by_id',
              classmethod(mock_get_interaction_by_id))
  
          with rules_registry_swap, interaction_registry_swap:
              html_list = state.get_all_html_content_strings()
  
          self.assertItemsEqual(
              html_list,
              [
                  '<p>state written_translation solution-hi</p>',
                  '<p>state written_translation solution-en</p>',
                  '<p>state written_translation content-hi</p>',
                  '<p>state written_translation content-en</p>',
                  '<p>state written_translation feedback-hi</p>',
                  '<p>state written_translation feedback-en</p>',
                  '<p>state written_translation hint_1-hi</p>',
                  '<p>state written_translation hint_1-en</p>',
                  '<p>state written_translation outcome-hi</p>',
                  '<p>state written_translation outcome-en</p>',
                  '<p>state written_translation ca_choices_0-hi</p>',
                  '<p>state written_translation ca_choices_0-en</p>',
                  '<p>state written_translation ca_choices_1-hi</p>',
                  '<p>state written_translation ca_choices_1-en</p>',
                  '<p>state written_translation ca_choices_2-hi</p>',
                  '<p>state written_translation ca_choices_2-en</p>',
                  '<p>state written_translation ca_choices_3-hi</p>',
                  '<p>state written_translation ca_choices_3-en</p>',
                  '<p>State Feedback</p>',
                  '<p>IsEqualToOrdering rule_spec htmls</p>',
                  '<p>HasElementXAtPositionY rule_spec html</p>',
                  '<p>y input for HasElementXAtPositionY rule_spec </p>',
                  '<p>x input for HasElementXAtPositionY rule_spec </p>',
                  (
                      '<p>IsEqualToOrderingWithOneItemAtIncorrectPosition rule_s'
                      'pec htmls</p>'),
                  '',
                  '<p>Hello, this is html1 for hint 1</p>',
                  '<p>This is solution for state1</p>',
                  '<p>state customization arg html 1</p>',
                  '<p>state customization arg html 2</p>',
                  '<p>state customization arg html 3</p>',
                  '<p>state customization arg html 4</p>',
                  '<p>state customization arg html 1</p>',
                  '<p>state customization arg html 2</p>',
                  '<p>state customization arg html 3</p>',
                  '<p>state customization arg html 4</p>',
                  '<p>state content html</p>'])
  
      def test_get_all_html_in_exploration_with_text_input_interaction(self):
          """Test the method for extracting all the HTML from a state having
          TextInput interaction.
          """
  
          exploration = exp_domain.Exploration.create_default_exploration(
              'exp_id')
          exploration.add_states(['State1'])
          state = exploration.states['State1']
  
          state_content_dict = {
              'content_id': 'content',
              'html': '<p>state content html</p>'
          }
          state_answer_group = [state_domain.AnswerGroup(
              state_domain.Outcome(
                  exploration.init_state_name, state_domain.SubtitledHtml(
                      'feedback_1', '<p>state outcome html</p>'),
                  False, [], None, None),
              [
                  state_domain.RuleSpec(
                      'Equals', {
                          'x': {
                              'contentId': 'rule_input_Equals',
                              'normalizedStrSet': ['Test']
                              }})
              ],
              [],
              None
          )]
          state_default_outcome = state_domain.Outcome(
              'State1', state_domain.SubtitledHtml(
                  'default_outcome', '<p>Default outcome for State1</p>'),
              False, [], None, None
          )
          state_hint_list = [
              state_domain.Hint(
                  state_domain.SubtitledHtml(
                      'hint_1', '<p>Hello, this is html1 for state1</p>'
                  )
              ),
              state_domain.Hint(
                  state_domain.SubtitledHtml(
                      'hint_2', '<p>Hello, this is html2 for state1</p>'
                  )
              ),
          ]
          state_solution_dict = {
              'answer_is_exclusive': True,
              'correct_answer': 'Answer1',
              'explanation': {
                  'content_id': 'solution',
                  'html': '<p>This is solution for state1</p>'
              }
          }
          state_interaction_cust_args = {
              'placeholder': {
                  'value': {
                      'content_id': 'ca_placeholder_0',
                      'unicode_str': ''
                  }
              },
              'rows': {'value': 1}
          }
  
          state.update_next_content_id_index(3)
          state.update_content(
              state_domain.SubtitledHtml.from_dict(state_content_dict))
          state.update_interaction_id('TextInput')
          state.update_interaction_customization_args(state_interaction_cust_args)
          state.update_interaction_answer_groups(
              state_answer_group)
          state.update_interaction_default_outcome(state_default_outcome)
          state.update_interaction_hints(state_hint_list)
          solution = state_domain.Solution.from_dict(
              state.interaction.id, state_solution_dict)
          state.update_interaction_solution(solution)
  
          exp_services.save_new_exploration('owner_id', exploration)
          html_list = state.get_all_html_content_strings()
          self.assertEqual(
              html_list,
              [
                  '<p>state outcome html</p>',
                  '<p>Default outcome for State1</p>',
                  '<p>Hello, this is html1 for state1</p>',
                  '<p>Hello, this is html2 for state1</p>',
                  '<p>This is solution for state1</p>',
                  '<p>state content html</p>'])
  
      def test_get_all_html_in_exploration_with_item_selection_interaction(self):
          """Test the method for extracting all the HTML from a state having
          ItemSelectionInput interaction.
          """
  
          exploration = exp_domain.Exploration.create_default_exploration(
              'exp_id')
          exploration.add_states(['State1'])
          state = exploration.states['State1']
  
          state_content_dict = {
              'content_id': 'content',
              'html': '<p>state content html</p>'
          }
          state_customization_args_dict = {
              'maxAllowableSelectionCount': {
                  'value': 1
              },
              'minAllowableSelectionCount': {
                  'value': 1
              },
              'choices': {
                  'value': [
                      {
                          'content_id': 'ca_choices_0',
                          'html': '<p>init_state customization arg html 1</p>'
                      }, {
                          'content_id': 'ca_choices_1',
                          'html': '<p>init_state customization arg html 2</p>'
                      }, {
                          'content_id': 'ca_choices_2',
                          'html': '<p>init_state customization arg html 3</p>'
                      }, {
                          'content_id': 'ca_choices_3',
                          'html': '<p>init_state customization arg html 4</p>'
                      },
                  ]
              }
          }
          state_answer_group = state_domain.AnswerGroup(
              state_domain.Outcome(
                  exploration.init_state_name, state_domain.SubtitledHtml(
                      'feedback', '<p>state outcome html</p>'),
                  False, [], None, None),
              [
                  state_domain.RuleSpec(
                      'Equals',
                      {
                          'x': ['<p>Equals rule_spec html</p>']
                      }),
                  state_domain.RuleSpec(
                      'ContainsAtLeastOneOf',
                      {
                          'x': ['<p>ContainsAtLeastOneOf rule_spec html</p>']
                      }),
                  state_domain.RuleSpec(
                      'IsProperSubsetOf',
                      {
                          'x': ['<p>IsProperSubsetOf rule_spec html</p>']
                      }),
                  state_domain.RuleSpec(
                      'DoesNotContainAtLeastOneOf',
                      {
                          'x': ['<p>DoesNotContainAtLeastOneOf rule_'
                                'spec html</p>']
                      })
              ],
              [],
              None
          )
          state_solution_dict = {
              'answer_is_exclusive': True,
              'correct_answer': [
                  '<p>state customization arg html 1</p>',
                  '<p>state customization arg html 2</p>',
                  '<p>state customization arg html 3</p>',
                  '<p>state customization arg html 4</p>'
              ],
              'explanation': {
                  'content_id': 'solution',
                  'html': '<p>This is solution for state1</p>'
              }
          }
          state_hint_list = [
              state_domain.Hint(
                  state_domain.SubtitledHtml(
                      'hint_1', '<p>Hello, this is html1 for hint 1</p>'
                  )
              )
          ]
  
          state.update_content(
              state_domain.SubtitledHtml.from_dict(state_content_dict))
          state.update_interaction_id('ItemSelectionInput')
          state.update_interaction_answer_groups([state_answer_group])
          state.update_interaction_customization_args(
              state_customization_args_dict)
          state.update_next_content_id_index(4)
          state.update_interaction_hints(state_hint_list)
  
          solution = state_domain.Solution.from_dict(
              state.interaction.id, state_solution_dict)
          state.update_interaction_solution(solution)
          exp_services.save_new_exploration('owner_id', exploration)
  
          mock_html_field_types_to_rule_specs_dict = (
              rules_registry.Registry.get_html_field_types_to_rule_specs(
                  state_schema_version=41))
  
          def mock_get_html_field_types_to_rule_specs(unused_cls):
              return mock_html_field_types_to_rule_specs_dict
  
          def mock_get_interaction_by_id(cls, interaction_id):
              interaction = copy.deepcopy(cls._interactions[interaction_id]) # pylint: disable=protected-access
              interaction.answer_type = 'SetOfHtmlString'
              interaction.can_have_solution = True
              return interaction
  
          rules_registry_swap = self.swap(
              rules_registry.Registry, 'get_html_field_types_to_rule_specs',
              classmethod(mock_get_html_field_types_to_rule_specs))
  
          interaction_registry_swap = self.swap(
              interaction_registry.Registry, 'get_interaction_by_id',
              classmethod(mock_get_interaction_by_id))
  
          with rules_registry_swap, interaction_registry_swap:
              html_list = state.get_all_html_content_strings()
  
          self.assertEqual(
              html_list,
              [
                  '<p>state outcome html</p>',
                  '<p>Equals rule_spec html</p>',
                  '<p>ContainsAtLeastOneOf rule_spec html</p>',
                  '<p>IsProperSubsetOf rule_spec html</p>',
                  '<p>DoesNotContainAtLeastOneOf rule_spec html</p>', '',
                  '<p>Hello, this is html1 for hint 1</p>',
                  '<p>This is solution for state1</p>',
                  '<p>state customization arg html 1</p>',
                  '<p>state customization arg html 2</p>',
                  '<p>state customization arg html 3</p>',
                  '<p>state customization arg html 4</p>',
                  '<p>init_state customization arg html 1</p>',
                  '<p>init_state customization arg html 2</p>',
                  '<p>init_state customization arg html 3</p>',
                  '<p>init_state customization arg html 4</p>',
                  '<p>state content html</p>'])
  
      def test_rule_spec_with_invalid_html_format(self):
          """Test the method for extracting all the HTML from a state
          when the rule_spec has invalid html format.
          """
  
          exploration = exp_domain.Exploration.create_default_exploration(
              'exp_id')
          exploration.add_states(['State1'])
          state = exploration.states['State1']
          state_answer_group = state_domain.AnswerGroup(
              state_domain.Outcome(
                  exploration.init_state_name, state_domain.SubtitledHtml(
                      'feedback', '<p>state outcome html</p>'),
                  False, [], None, None),
              [
                  state_domain.RuleSpec(
                      'Equals',
                      {
                          'x': ['<p>Equals rule_spec html</p>']
                      }),
                  state_domain.RuleSpec(
                      'ContainsAtLeastOneOf',
                      {
                          'x': ['<p>ContainsAtLeastOneOf rule_spec html</p>']
  
                      }),
                  state_domain.RuleSpec(
                      'IsProperSubsetOf',
                      {
                          'x': ['<p>IsProperSubsetOf rule_spec html</p>']
                      }),
                  state_domain.RuleSpec(
                      'DoesNotContainAtLeastOneOf',
                      {
                          'x': ['<p>DoesNotContainAtLeastOneOf rule_'
                                'spec html</p>']
                      })
              ],
              [],
              None
          )
  
          state.update_interaction_id('ItemSelectionInput')
          state.update_interaction_answer_groups([state_answer_group])
  
          mock_html_field_types_to_rule_specs_dict = copy.deepcopy(
              rules_registry.Registry.get_html_field_types_to_rule_specs(
                  state_schema_version=41))
          for html_type_dict in (
                  mock_html_field_types_to_rule_specs_dict.values()):
              html_type_dict['format'] = 'invalid format'
  
          def mock_get_html_field_types_to_rule_specs(unused_cls):
              return mock_html_field_types_to_rule_specs_dict
  
          with self.swap(
              rules_registry.Registry, 'get_html_field_types_to_rule_specs',
              classmethod(mock_get_html_field_types_to_rule_specs)
          ):
              with self.assertRaisesRegexp(
                  Exception,
                  'The rule spec does not belong to a valid format.'):
                  state.get_all_html_content_strings()
  
      def test_update_customization_args_with_invalid_content_id(self):
          """Test the method for updating interaction customization arguments
          when a content_id is invalid (set to None).
          """
  
          exploration = exp_domain.Exploration.create_default_exploration(
              'exp_id')
          exploration.add_states(['State1'])
          state = exploration.states['State1']
          state_customization_args_dict = {
              'maxAllowableSelectionCount': {
                  'value': 1
              },
              'minAllowableSelectionCount': {
                  'value': 1
              },
              'choices': {
                  'value': [
                      {
                          'content_id': None,
                          'html': '<p>init_state customization arg html 1</p>'
                      }, {
                          'content_id': 'ca_choices_1',
                          'html': '<p>init_state customization arg html 2</p>'
                      }
                  ]
              }
          }
  
          state.update_interaction_id('ItemSelectionInput')
          with self.assertRaisesRegexp(
              utils.ValidationError,
              'Expected content id to be a string, received None'
          ):
              state.update_interaction_customization_args(
                  state_customization_args_dict)
  
      def test_rule_spec_with_html_having_invalid_input_variable(self):
          """Test the method for extracting all the HTML from a state
          when the rule_spec has html but the input variable is invalid.
          """
  
          exploration = exp_domain.Exploration.create_default_exploration(
              'exp_id')
          exploration.add_states(['State1'])
          state = exploration.states['State1']
          state_answer_group = state_domain.AnswerGroup(
              state_domain.Outcome(
                  exploration.init_state_name, state_domain.SubtitledHtml(
                      'feedback', '<p>state outcome html</p>'),
                  False, [], None, None),
              [
                  state_domain.RuleSpec(
                      'Equals',
                      {
                          'x': ['<p>init_state customization arg html 1</p>']
                      })
              ],
              [],
              None
          )
          state_customization_args_dict = {
              'maxAllowableSelectionCount': {
                  'value': 1
              },
              'minAllowableSelectionCount': {
                  'value': 1
              },
              'choices': {
                  'value': [
                      {
                          'content_id': 'ca_choices_0',
                          'html': '<p>init_state customization arg html 1</p>'
                      }, {
                          'content_id': 'ca_choices_1',
                          'html': '<p>init_state customization arg html 2</p>'
                      }, {
                          'content_id': 'ca_choices_2',
                          'html': '<p>init_state customization arg html 3</p>'
                      }, {
                          'content_id': 'ca_choices_3',
                          'html': '<p>init_state customization arg html 4</p>'
                      }
                  ]
              }
          }
  
          state.update_interaction_id('ItemSelectionInput')
          state.update_interaction_customization_args(
              state_customization_args_dict)
          state.update_interaction_answer_groups([state_answer_group])
  
          mock_html_field_types_to_rule_specs_dict = copy.deepcopy(
              rules_registry.Registry.get_html_field_types_to_rule_specs(
                  state_schema_version=41))
          for html_type_dict in (
                  mock_html_field_types_to_rule_specs_dict.values()):
              if html_type_dict['interactionId'] == 'ItemSelectionInput':
                  html_type_dict['ruleTypes']['Equals']['htmlInputVariables'] = (
                      ['y'])
  
          def mock_get_html_field_types_to_rule_specs(unused_cls):
              return mock_html_field_types_to_rule_specs_dict
  
          with self.swap(
              rules_registry.Registry, 'get_html_field_types_to_rule_specs',
              classmethod(mock_get_html_field_types_to_rule_specs)
          ):
              with self.assertRaisesRegexp(
                  Exception,
                  'Rule spec should have at least one valid input variable with '
                  'Html in it.'):
                  state.get_all_html_content_strings()
  
      def test_get_all_html_when_solution_has_invalid_answer_type(self):
          """Test the method for extracting all the HTML from a state
          when the interaction has a solution but the answer_type for the
          corrent_answer is invalid.
          """
          exploration = exp_domain.Exploration.create_default_exploration(
              'exp_id')
          exploration.add_states(['State1'])
          state = exploration.states['State1']
          state_content_dict = {
              'content_id': 'content',
              'html': '<p>state content html</p>'
          }
          state_customization_args_dict = {
              'choices': {
                  'value': [
                      {
                          'content_id': 'ca_choices_0',
                          'html': '<p>state customization arg html 1</p>'
                      }, {
                          'content_id': 'ca_choices_1',
                          'html': '<p>state customization arg html 2</p>'
                      }, {
                          'content_id': 'ca_choices_2',
                          'html': '<p>state customization arg html 3</p>'
                      }, {
                          'content_id': 'ca_choices_3',
                          'html': '<p>state customization arg html 4</p>'
                      }
                  ]
              },
              'allowMultipleItemsInSamePosition': {
                  'value': False
              }
          }
  
          state_hint_list = [
              state_domain.Hint(
                  state_domain.SubtitledHtml(
                      'hint_1', '<p>Hello, this is html1 for hint 1</p>'
                  )
              )
          ]
  
          state_solution_dict = {
              'interaction_id': '',
              'answer_is_exclusive': True,
              'correct_answer': [
                  ['<p>state customization arg html 1</p>'],
                  ['<p>state customization arg html 2</p>'],
                  ['<p>state customization arg html 3</p>'],
                  ['<p>state customization arg html 4</p>']
              ],
              'explanation': {
                  'content_id': 'solution',
                  'html': '<p>This is solution for state1</p>'
              }
          }
  
          state.update_content(
              state_domain.SubtitledHtml.from_dict(state_content_dict))
          state.update_interaction_id('DragAndDropSortInput')
          state.update_interaction_customization_args(
              state_customization_args_dict)
          state.update_next_content_id_index(4)
          state.update_interaction_hints(state_hint_list)
          solution = state_domain.Solution.from_dict(
              state.interaction.id, state_solution_dict)
          state.update_interaction_solution(solution)
          exp_services.save_new_exploration('owner_id', exploration)
  
          interaction = (
              interaction_registry.Registry.get_interaction_by_id(
                  'DragAndDropSortInput'))
          interaction.answer_type = 'DragAndDropHtmlString'
  
          mock_html_field_types_to_rule_specs_dict = copy.deepcopy(
              rules_registry.Registry.get_html_field_types_to_rule_specs(
                  state_schema_version=41))
  
          def mock_get_html_field_types_to_rule_specs(unused_cls):
              return mock_html_field_types_to_rule_specs_dict
  
          with self.swap(
              rules_registry.Registry, 'get_html_field_types_to_rule_specs',
              classmethod(mock_get_html_field_types_to_rule_specs)):
              with self.assertRaisesRegexp(
                  Exception,
                  'The solution does not have a valid '
                  'correct_answer type.'):
                  state.get_all_html_content_strings()
  
      def test_get_all_html_when_interaction_is_none(self):
          """Test the method for extracting all the HTML from a state
          when the state has no interaction.
          """
          exploration = exp_domain.Exploration.create_default_exploration(
              'exp_id')
          exploration.add_states(['State1'])
          state = exploration.states['State1']
          state_content_dict = {
              'content_id': 'content',
              'html': '<p>state content html</p>'
          }
  
          state.update_content(
              state_domain.SubtitledHtml.from_dict(state_content_dict))
  
          exp_services.save_new_exploration('owner_id', exploration)
          html_list = state.get_all_html_content_strings()
          self.assertEqual(html_list, ['', '<p>state content html</p>'])
  
      def test_export_state_to_dict(self):
          """Test exporting a state to a dict."""
          exploration = exp_domain.Exploration.create_default_exploration(
              'exp_id')
          exploration.add_states(['New state'])
  
          state_dict = exploration.states['New state'].to_dict()
          expected_dict = {
              'classifier_model_id': None,
              'content': {
                  'content_id': 'content',
                  'html': ''
              },
              'interaction': {
                  'answer_groups': [],
                  'confirmed_unclassified_answers': [],
                  'customization_args': {},
                  'default_outcome': {
                      'dest': 'New state',
                      'feedback': {
                          'content_id': 'default_outcome',
                          'html': ''
                      },
                      'labelled_as_correct': False,
                      'param_changes': [],
                      'refresher_exploration_id': None,
                      'missing_prerequisite_skill_id': None
                  },
                  'hints': [],
                  'id': None,
                  'solution': None,
              },
              'linked_skill_id': None,
              'next_content_id_index': 0,
              'param_changes': [],
              'recorded_voiceovers': {
                  'voiceovers_mapping': {
                      'content': {},
                      'default_outcome': {}
                  }
              },
              'solicit_answer_details': False,
              'card_is_checkpoint': False,
              'written_translations': {
                  'translations_mapping': {
                      'content': {},
                      'default_outcome': {}
                  }
              }
          }
          self.assertEqual(expected_dict, state_dict)
  
      def test_can_undergo_classification(self):
          """Test the can_undergo_classification() function."""
          exploration_id = 'eid'
          test_exp_filepath = os.path.join(
              feconf.TESTS_DATA_DIR, 'string_classifier_test.yaml')
          yaml_content = utils.get_file_contents(test_exp_filepath)
          assets_list = []
          exp_services.save_new_exploration_from_yaml_and_assets(
              feconf.SYSTEM_COMMITTER_ID, yaml_content, exploration_id,
              assets_list)
  
          exploration = exp_fetchers.get_exploration_by_id(exploration_id)
          state_with_training_data = exploration.states['Home']
          state_without_training_data = exploration.states['End']
  
          # A state with 786 training examples.
          self.assertTrue(
              state_with_training_data.can_undergo_classification())
  
          # A state with no training examples.
          self.assertFalse(
              state_without_training_data.can_undergo_classification())
  
      def test_get_training_data(self):
          """Test retrieval of training data."""
          exploration_id = 'eid'
          test_exp_filepath = os.path.join(
              feconf.SAMPLE_EXPLORATIONS_DIR, 'classifier_demo_exploration.yaml')
          yaml_content = utils.get_file_contents(test_exp_filepath)
          assets_list = []
          exp_services.save_new_exploration_from_yaml_and_assets(
              feconf.SYSTEM_COMMITTER_ID, yaml_content, exploration_id,
              assets_list)
  
          exploration = exp_fetchers.get_exploration_by_id(exploration_id)
          state = exploration.states['text']
  
          expected_training_data = [{
              'answer_group_index': 1,
              'answers': [u'cheerful', u'merry', u'ecstatic', u'glad',
                          u'overjoyed', u'pleased', u'thrilled', u'smile']}]
  
          observed_training_data = state.get_training_data()
  
          self.assertEqual(observed_training_data, expected_training_data)
  
      def test_get_content_html_with_correct_state_name_returns_html(self):
          exploration = exp_domain.Exploration.create_default_exploration('0')
  
          init_state = exploration.states[exploration.init_state_name]
          init_state.update_interaction_id('TextInput')
          hints_list = [
              state_domain.Hint(
                  state_domain.SubtitledHtml('hint_1', '<p>hint one</p>')
              )
          ]
          init_state.update_interaction_hints(hints_list)
  
          self.assertEqual(
              init_state.get_content_html('hint_1'), '<p>hint one</p>')
  
          hints_list[0].hint_content.html = '<p>Changed hint one</p>'
          init_state.update_interaction_hints(hints_list)
  
          self.assertEqual(
              init_state.get_content_html('hint_1'), '<p>Changed hint one</p>')
  
      def test_rte_content_validation_for_android(self):
          exploration = exp_domain.Exploration.create_default_exploration('0')
  
          init_state = exploration.states[exploration.init_state_name]
          init_state.update_interaction_id('TextInput')
          solution_dict = {
              'answer_is_exclusive': False,
              'correct_answer': 'helloworld!',
              'explanation': {
                  'content_id': 'solution',
                  'html': (
                      '<oppia-noninteractive-collapsible content-with-value='
                      '"&amp;quot;&amp;lt;p&amp;gt;Hello&amp;lt;/p&amp;gt;&amp;'
                      'quot;" heading-with-value="&amp;quot;SubCollapsible&amp;'
                      'quot;"></oppia-noninteractive-collapsible><p>&nbsp;</p>')
              },
          }
  
          solution = state_domain.Solution.from_dict(
              init_state.interaction.id, solution_dict
          )
          init_state.update_interaction_solution(solution)
          self.assertFalse(init_state.is_rte_content_supported_on_android())
          solution_dict['explanation']['html'] = ''
          init_state.update_interaction_solution(state_domain.Solution.from_dict(
              init_state.interaction.id, solution_dict))
          self.assertTrue(init_state.is_rte_content_supported_on_android())
  
          hints_list = []
          hints_list.append(
              state_domain.Hint(
                  state_domain.SubtitledHtml(
                      'hint_1',
                      '<oppia-noninteractive-collapsible content-with-value='
                      '"&amp;quot;&amp;lt;p&amp;gt;Hello&amp;lt;/p&amp;gt;&amp;'
                      'quot;" heading-with-value="&amp;quot;SubCollapsible&amp;'
                      'quot;"></oppia-noninteractive-collapsible><p>&nbsp;</p>'
                  )
              )
          )
          init_state.update_interaction_hints(hints_list)
          self.assertFalse(init_state.is_rte_content_supported_on_android())
          hints_list[0].hint_content.html = ''
          init_state.update_interaction_hints(hints_list)
          self.assertTrue(init_state.is_rte_content_supported_on_android())
  
          default_outcome = state_domain.Outcome(
              'Introduction', state_domain.SubtitledHtml(
                  'default_outcome', (
                      '<oppia-noninteractive-collapsible content-with-value='
                      '"&amp;quot;&amp;lt;p&amp;gt;Hello&amp;lt;/p&amp;gt;&amp;'
                      'quot;" heading-with-value="&amp;quot;Sub&amp;quot;">'
                      '</oppia-noninteractive-collapsible><p>&nbsp;</p>')),
              False, [], None, None
          )
  
          init_state.update_interaction_default_outcome(default_outcome)
          self.assertFalse(init_state.is_rte_content_supported_on_android())
          default_outcome.feedback.html = ''
          init_state.update_interaction_default_outcome(default_outcome)
          self.assertTrue(init_state.is_rte_content_supported_on_android())
  
          state_answer_group = state_domain.AnswerGroup(
              state_domain.Outcome(
                  exploration.init_state_name, state_domain.SubtitledHtml(
                      'feedback_1', (
                          '<oppia-noninteractive-tabs tab_contents-with-value'
                          '=\"[{&amp;quot;content&amp;quot;:&amp;quot;&amp;lt;p'
                          '&amp;gt;&amp;lt;i&amp;gt;lorem ipsum&amp;lt;/i&amp;'
                          'gt;&amp;lt;/p&amp;gt;&amp;quot;,&amp;quot;title&amp;'
                          'quot;:&amp;quot;hello&amp;quot;}]\">'
                          '</oppia-noninteractive-tabs>')),
                  False, [], None, None),
              [
                  state_domain.RuleSpec(
                      'Contains',
                      {
                          'x': {
                              'contentId': 'rule_input_Equals',
                              'normalizedStrSet': ['Test']
                              }
                      })
              ],
              [],
              None
          )
  
          init_state.update_interaction_answer_groups(
              [state_answer_group])
          self.assertFalse(init_state.is_rte_content_supported_on_android())
          state_answer_group.outcome.feedback.html = (
              '<p><oppia-noninteractive-image caption-with-value="&amp;quot;'
              '&amp;quot;" filepath-with-value="&amp;quot;startBlue.png&amp;'
              'quot;" alt-with-value="&amp;quot;&amp;quot;">'
              '</oppia-noninteractive-image></p>')
          init_state.update_interaction_answer_groups(
              [state_answer_group])
          self.assertTrue(init_state.is_rte_content_supported_on_android())
  
          init_state.update_content(
              state_domain.SubtitledHtml.from_dict({
                  'content_id': 'content',
                  'html': (
                      '<oppia-noninteractive-tabs tab_contents-with-value'
                      '=\"[{&amp;quot;content&amp;quot;:&amp;quot;&amp;lt;p'
                      '&amp;gt;&amp;lt;i&amp;gt;lorem ipsum&amp;lt;/i&amp;'
                      'gt;&amp;lt;/p&amp;gt;&amp;quot;,&amp;quot;title&amp;'
                      'quot;:&amp;quot;hello&amp;quot;}]\">'
                      '</oppia-noninteractive-tabs>')
              }))
          self.assertFalse(init_state.is_rte_content_supported_on_android())
          init_state.update_content(
              state_domain.SubtitledHtml.from_dict({
                  'content_id': 'content',
                  'html': (
                      '<p><oppia-noninteractive-link text-with-value="'
                      '&amp;quot;What is a link?&amp;quot;" url-with-'
                      'value="&amp;quot;htt://link.com&amp'
                      ';quot;"></oppia-noninteractive-link></p>')
              }))
          self.assertTrue(init_state.is_rte_content_supported_on_android())
          init_state.update_content(
              state_domain.SubtitledHtml.from_dict({
                  'content_id': 'content',
                  'html': (
                      '<p><oppia-noninteractive-skillreview text-with-value="'
                      '&amp;quot;&amp;quot;" skill_id-with-value="&amp;quot;'
                      '&amp;quot;"></oppia-noninteractive-skillreview></p>')
              }))
          self.assertTrue(init_state.is_rte_content_supported_on_android())
  
      def test_interaction_validation_for_android(self):
          _checked_interaction_ids = set()
  
          def _create_init_state_for_interaction_verification():
              """Creates an init state for interaction verification."""
              exploration = (
                  exp_domain.Exploration.create_default_exploration('0'))
              return exploration.states[exploration.init_state_name]
  
          def _verify_interaction_supports_android(self, interaction_id):
              """Checks that the provided interaction is supported on Android."""
              init_state = _create_init_state_for_interaction_verification()
              init_state.update_interaction_id(interaction_id)
              self.assertTrue(
                  init_state.interaction.is_supported_on_android_app())
              _checked_interaction_ids.add(interaction_id)
  
          def _verify_interaction_does_not_support_android(self, interaction_id):
              """Checks that the provided interaction is not supported on
              Android.
              """
              init_state = _create_init_state_for_interaction_verification()
              init_state.update_interaction_id(interaction_id)
              self.assertFalse(
                  init_state.interaction.is_supported_on_android_app())
              _checked_interaction_ids.add(interaction_id)
  
          def _verify_all_interaction_ids_checked(self):
              """Verifies that all the interaction ids are checked."""
              all_interaction_ids = set(
                  interaction_registry.Registry.get_all_interaction_ids())
              missing_interaction_ids = (
                  all_interaction_ids - _checked_interaction_ids)
              self.assertFalse(missing_interaction_ids)
  
          _verify_interaction_supports_android(self, 'AlgebraicExpressionInput')
          _verify_interaction_supports_android(self, 'Continue')
          _verify_interaction_supports_android(self, 'DragAndDropSortInput')
          _verify_interaction_supports_android(self, 'EndExploration')
          _verify_interaction_supports_android(self, 'FractionInput')
          _verify_interaction_supports_android(self, 'ImageClickInput')
          _verify_interaction_supports_android(self, 'ItemSelectionInput')
          _verify_interaction_supports_android(self, 'MathEquationInput')
          _verify_interaction_supports_android(self, 'MultipleChoiceInput')
          _verify_interaction_supports_android(self, 'NumberWithUnits')
          _verify_interaction_supports_android(self, 'NumericInput')
          _verify_interaction_supports_android(self, 'TextInput')
          _verify_interaction_supports_android(self, 'NumericExpressionInput')
          _verify_interaction_supports_android(self, 'RatioExpressionInput')
          _verify_interaction_supports_android(self, None)
  
          _verify_interaction_does_not_support_android(self, 'CodeRepl')
          _verify_interaction_does_not_support_android(self, 'GraphInput')
          _verify_interaction_does_not_support_android(self, 'InteractiveMap')
          _verify_interaction_does_not_support_android(self, 'LogicProof')
          _verify_interaction_does_not_support_android(self, 'MusicNotesInput')
          _verify_interaction_does_not_support_android(self, 'PencilCodeEditor')
          _verify_interaction_does_not_support_android(self, 'SetInput')
  
          _verify_all_interaction_ids_checked(self)
  
      def test_get_content_html_with_invalid_content_id_raise_error(self):
          exploration = exp_domain.Exploration.create_default_exploration('0')
          init_state = exploration.states[exploration.init_state_name]
          init_state.update_interaction_id('TextInput')
          hints_list = [
              state_domain.Hint(
                  state_domain.SubtitledHtml('hint_1', '<p>hint one</p>')
              )
          ]
          init_state.update_interaction_hints(hints_list)
  
          self.assertEqual(
              init_state.get_content_html('hint_1'), '<p>hint one</p>')
  
          with self.assertRaisesRegexp(
              ValueError, 'Content ID Invalid id does not exist'):
              init_state.get_content_html('Invalid id')
  
      def test_get_content_id_mapping_needing_translations_with_existing_translations(self): # pylint: disable=line-too-long
          exploration = exp_domain.Exploration.create_default_exploration('0')
          init_state = exploration.states[exploration.init_state_name]
          init_state.update_content(
              state_domain.SubtitledHtml.from_dict({
                  'content_id': 'content',
                  'html': '<p>This is content</p>'
              }))
          init_state.update_interaction_id('TextInput')
          default_outcome = state_domain.Outcome(
              'Introduction', state_domain.SubtitledHtml(
                  'default_outcome', '<p>The default outcome.</p>'),
              False, [], None, None
          )
  
          init_state.update_interaction_default_outcome(default_outcome)
  
          state_answer_group = state_domain.AnswerGroup(
              state_domain.Outcome(
                  exploration.init_state_name, state_domain.SubtitledHtml(
                      'feedback_1', '<p>Feedback</p>'), False, [], None, None),
              [
                  state_domain.RuleSpec(
                      'Contains',
                      {
                          'x': {
                              'contentId': 'rule_input_Equals',
                              'normalizedStrSet': ['Test']
                              }
                      })
              ],
              [],
              None
          )
  
          init_state.update_interaction_answer_groups(
              [state_answer_group])
          hints_list = [
              state_domain.Hint(
                  state_domain.SubtitledHtml('hint_1', '<p>hint one</p>')
              )
          ]
          init_state.update_interaction_hints(hints_list)
  
          solution_dict = {
              'answer_is_exclusive': False,
              'correct_answer': 'helloworld!',
              'explanation': {
                  'content_id': 'solution',
                  'html': '<p>hello_world is a string</p>'
              },
          }
  
          solution = state_domain.Solution.from_dict(
              init_state.interaction.id, solution_dict)
          init_state.update_interaction_solution(solution)
  
          written_translations_dict = {
              'translations_mapping': {
                  'content': {
                      'hi': {
                          'data_format': 'html',
                          'translation': '<p>hello!</p>',
                          'needs_update': False
                      }
                  },
                  'hint_1': {},
                  'default_outcome': {},
                  'solution': {},
                  'feedback_1': {}
              }
          }
          written_translations = state_domain.WrittenTranslations.from_dict(
              written_translations_dict)
  
          init_state.update_written_translations(written_translations)
  
          content_id_mapping_needing_translations = (
              init_state.get_content_id_mapping_needing_translations('hi'))
          self.assertEqual(
              content_id_mapping_needing_translations[
                  'hint_1'
              ].content,
              '<p>hint one</p>'
          )
          self.assertEqual(
              content_id_mapping_needing_translations[
                  'solution'
              ].content,
              '<p>hello_world is a string</p>'
          )
          self.assertEqual(
              content_id_mapping_needing_translations[
                  'feedback_1'
              ].content,
              '<p>Feedback</p>',
          )
          self.assertEqual(
              content_id_mapping_needing_translations[
                  'default_outcome'
              ].content,
              '<p>The default outcome.</p>'
          )
  
      def test_get_content_id_mapping_needing_translations_with_interaction_translations(self): # pylint: disable=line-too-long
          exploration = exp_domain.Exploration.create_default_exploration('0')
          init_state = exploration.states[exploration.init_state_name]
          init_state.update_content(
              state_domain.SubtitledHtml.from_dict({
                  'content_id': 'content',
                  'html': '<p>This is content</p>'
              }))
          init_state.update_interaction_id('TextInput')
          state_interaction_cust_args = {
              'placeholder': {
                  'value': {
                      'content_id': 'ca_placeholder_0',
                      'unicode_str': 'Placeholder'
                  }
              },
              'rows': {'value': 1}
          }
          init_state.update_interaction_customization_args(
              state_interaction_cust_args)
  
          default_outcome = state_domain.Outcome(
              'Introduction', state_domain.SubtitledHtml(
                  'default_outcome', '<p>The default outcome.</p>'),
              False, [], None, None
          )
  
          init_state.update_interaction_default_outcome(default_outcome)
          state_answer_group = state_domain.AnswerGroup(
              state_domain.Outcome(
                  exploration.init_state_name, state_domain.SubtitledHtml(
                      'feedback_1', '<p>Feedback</p>'),
                  False, [], None, None),
              [
                  state_domain.RuleSpec(
                      'Contains',
                      {
                          'x': {
                              'contentId': 'rule_input_4',
                              'normalizedStrSet': ['Input1', 'Input2']
                              }
                      })
              ],
              [],
              None
          )
  
          init_state.update_interaction_answer_groups(
              [state_answer_group])
          hints_list = [
              state_domain.Hint(
                  state_domain.SubtitledHtml('hint_1', '<p>hint one</p>')
              )
          ]
          init_state.update_interaction_hints(hints_list)
  
          solution_dict = {
              'answer_is_exclusive': False,
              'correct_answer': 'helloworld!',
              'explanation': {
                  'content_id': 'solution',
                  'html': '<p>hello_world is a string</p>'
              },
          }
  
          solution = state_domain.Solution.from_dict(
              init_state.interaction.id, solution_dict)
          init_state.update_interaction_solution(solution)
  
          written_translations_dict = {
              'translations_mapping': {
                  'content': {},
                  'hint_1': {},
                  'default_outcome': {},
                  'solution': {},
                  'feedback_1': {},
                  'ca_placeholder_0': {},
                  'rule_input_4': {}
              }
          }
          written_translations = state_domain.WrittenTranslations.from_dict(
              written_translations_dict)
  
          init_state.update_written_translations(written_translations)
  
          content_id_mapping_needing_translations = (
              init_state.get_content_id_mapping_needing_translations('hi'))
          self.assertEqual(
              content_id_mapping_needing_translations[
                  'hint_1'
              ].content,
              '<p>hint one</p>'
          )
          self.assertEqual(
              content_id_mapping_needing_translations[
                  'solution'
              ].content,
              '<p>hello_world is a string</p>'
          )
          self.assertEqual(
              content_id_mapping_needing_translations[
                  'feedback_1'
              ].content,
              '<p>Feedback</p>'
          )
          self.assertEqual(
              content_id_mapping_needing_translations[
                  'default_outcome'
              ].content,
              '<p>The default outcome.</p>'
          )
          self.assertEqual(
              content_id_mapping_needing_translations[
                  'content'
              ].content,
              '<p>This is content</p>',
          )
          self.assertEqual(
              content_id_mapping_needing_translations[
                  'ca_placeholder_0'
              ].content,
              'Placeholder'
          )
          rule_translatable_item = content_id_mapping_needing_translations[
              'rule_input_4'
          ]
          self.assertEqual(rule_translatable_item.content, ['Input1', 'Input2'])
          self.assertEqual(rule_translatable_item.interaction_id, 'TextInput')
          self.assertEqual(rule_translatable_item.rule_type, 'Contains')
  
      def test_get_content_id_mapping_needing_translations_for_set_input_rule(self): # pylint: disable=line-too-long
          exploration = exp_domain.Exploration.create_default_exploration('0')
          init_state = exploration.states[exploration.init_state_name]
          init_state.update_content(
              state_domain.SubtitledHtml.from_dict({
                  'content_id': 'content',
                  'html': '<p>This is content</p>'
              }))
          init_state.update_interaction_id('SetInput')
  
          state_answer_group = state_domain.AnswerGroup(
              state_domain.Outcome(
                  exploration.init_state_name, state_domain.SubtitledHtml(
                      'feedback_1', '<p>Feedback</p>'),
                  False, [], None, None),
              [
                  state_domain.RuleSpec(
                      'Equals',
                      {
                          'x': {
                              'contentId': 'rule_input_4',
                              'unicodeStrSet': ['Input1', 'Input2']
                              }
                      })
              ],
              [],
              None
          )
          init_state.update_interaction_answer_groups(
              [state_answer_group])
  
          written_translations_dict = {
              'translations_mapping': {
                  'content': {},
                  'feedback_1': {},
                  'rule_input_4': {}
              }
          }
          written_translations = state_domain.WrittenTranslations.from_dict(
              written_translations_dict)
          init_state.update_written_translations(written_translations)
  
          content_id_mapping_needing_translations = (
              init_state.get_content_id_mapping_needing_translations('hi'))
          rule_translatable_item = content_id_mapping_needing_translations[
              'rule_input_4'
          ]
          self.assertEqual(rule_translatable_item.content, ['Input1', 'Input2'])
          self.assertEqual(rule_translatable_item.interaction_id, 'SetInput')
          self.assertEqual(rule_translatable_item.rule_type, 'Equals')
  
      def test_get_content_id_mapping_needing_translations_does_not_return_numeric_content(self): # pylint: disable=line-too-long
          exploration = exp_domain.Exploration.create_default_exploration('0')
          init_state = exploration.states[exploration.init_state_name]
          # Set the content.
          init_state.update_content(
              state_domain.SubtitledHtml.from_dict({
                  'content_id': 'content',
                  'html': '<p>This is content</p>'
              }))
          # Set the multiple choice interaction.
          init_state.update_interaction_id('MultipleChoiceInput')
          state_interaction_cust_args = {
              'showChoicesInShuffledOrder': {
                  'value': True
              },
              'choices': {
                  'value': [
                      {
                          'content_id': 'ca_choices_0',
                          'html': '\u003cp\u003eoption 1\u003c/p\u003e'
                      },
                      {
                          'content_id': 'ca_choices_1',
                          'html': '1,000'
                      },
                      {
                          'content_id': 'ca_choices_2',
                          'html': '100'
                      }
                  ]
              }
          }
          init_state.update_interaction_customization_args(
              state_interaction_cust_args)
          # Set the default outcome.
          default_outcome = state_domain.Outcome(
              'Introduction', state_domain.SubtitledHtml(
                  'default_outcome', '<p>The default outcome.</p>'),
              False, [], None, None
          )
          init_state.update_interaction_default_outcome(default_outcome)
          # Set the translations.
          written_translations_dict = {
              'translations_mapping': {
                  'content': {},
                  'default_outcome': {},
                  'ca_choices_0': {},
                  'ca_choices_1': {},
                  'ca_choices_2': {}
              }
          }
          written_translations = state_domain.WrittenTranslations.from_dict(
              written_translations_dict)
          init_state.update_written_translations(written_translations)
  
          # Choice 2 should not be returned as its value is numeric.
          content_id_mapping_needing_translations = (
              init_state.get_content_id_mapping_needing_translations('hi'))
          self.assertEqual(
              content_id_mapping_needing_translations[
                  'content'
              ].content, '<p>This is content</p>')
          self.assertEqual(
              content_id_mapping_needing_translations[
                  'default_outcome'
              ].content, '<p>The default outcome.</p>')
          self.assertEqual(
              content_id_mapping_needing_translations[
                  'ca_choices_0'
              ].content, '\u003cp\u003eoption 1\u003c/p\u003e')
          self.assertEqual(
              content_id_mapping_needing_translations[
                  'ca_choices_1'
              ].content, '1,000')
          self.assertFalse(
              'ca_choices_2' in content_id_mapping_needing_translations)
  
      def test_add_translation_works_correctly(self):
          exploration = exp_domain.Exploration.create_default_exploration('0')
          init_state = exploration.states[exploration.init_state_name]
          init_state.update_content(
              state_domain.SubtitledHtml.from_dict({
                  'content_id': 'content',
                  'html': '<p>This is content</p>'
              }))
  
          self.assertEqual(init_state.get_translation_counts(), {})
  
          init_state.add_translation('content', 'hi', '<p>Translated text</p>')
  
          self.assertEqual(init_state.get_translation_counts(), {'hi': 1})
  
      def test_get_translation_counts_returns_correct_value(self):
          state = state_domain.State.create_default_state(None)
          state.update_content(
              state_domain.SubtitledHtml.from_dict({
                  'content_id': 'content',
                  'html': '<p>This is content</p>'
              }))
  
          self.set_interaction_for_state(state, 'TextInput')
  
          hints_list = [
              state_domain.Hint(
                  state_domain.SubtitledHtml('hint_1', '<p>hint one</p>'))]
          state.update_interaction_hints(hints_list)
  
          solution_dict = {
              'answer_is_exclusive': False,
              'correct_answer': 'helloworld!',
              'explanation': {
                  'content_id': 'solution',
                  'html': '<p>hello_world is a string</p>'
              },
          }
  
          solution = state_domain.Solution.from_dict(
              state.interaction.id, solution_dict)
  
          state.update_interaction_solution(solution)
          state.validate({}, True)
          state.add_translation('hint_1', 'hi', 'Some translation')
          state.add_translation('content', 'hi', 'Some translation')
  
          self.assertEqual(state.get_translation_counts(), {'hi': 2})
  
          # Adding interaction placeholder translation won't be reflected in
          # get_translation_counts method.
          state.add_translation('ca_placeholder_0', 'hi', 'Some translation')
  
          self.assertEqual(state.get_translation_counts(), {'hi': 2})
  
      def test_state_operations(self):
          """Test adding, updating and checking existence of states."""
          exploration = exp_domain.Exploration.create_default_exploration('eid')
          self.assertNotIn('invalid_state_name', exploration.states)
  
          self.assertEqual(len(exploration.states), 1)
  
          default_state_name = exploration.init_state_name
          exploration.rename_state(default_state_name, 'Renamed state')
          self.assertEqual(len(exploration.states), 1)
          self.assertEqual(exploration.init_state_name, 'Renamed state')
  
          # Add a new state.
          exploration.add_states(['State 2'])
          self.assertEqual(len(exploration.states), 2)
  
          # It is OK to rename a state to the same name.
          exploration.rename_state('State 2', 'State 2')
  
          # But it is not OK to add or rename a state using a name that already
          # exists.
          with self.assertRaisesRegexp(ValueError, 'Duplicate state name'):
              exploration.add_states(['State 2'])
          with self.assertRaisesRegexp(ValueError, 'Duplicate state name'):
              exploration.rename_state('State 2', 'Renamed state')
  
          # And it is OK to rename a state to 'END' (old terminal pseudostate). It
          # is tested throughout this test because a lot of old behavior used to
          # be specific to states named 'END'. These tests validate that is no
          # longer the situation.
          exploration.rename_state('State 2', 'END')
  
          # Should successfully be able to name it back.
          exploration.rename_state('END', 'State 2')
  
          # The exploration now has exactly two states.
          self.assertNotIn(default_state_name, exploration.states)
          self.assertIn('Renamed state', exploration.states)
          self.assertIn('State 2', exploration.states)
  
          # Can successfully add 'END' state.
          exploration.add_states(['END'])
  
          # Should fail to rename like any other state.
          with self.assertRaisesRegexp(ValueError, 'Duplicate state name'):
              exploration.rename_state('State 2', 'END')
  
          # Ensure the other states are connected to END.
          exploration.states[
              'Renamed state'].interaction.default_outcome.dest = 'State 2'
          exploration.states['State 2'].interaction.default_outcome.dest = 'END'
  
          # Ensure the other states have interactions.
          self.set_interaction_for_state(
              exploration.states['Renamed state'], 'TextInput')
          self.set_interaction_for_state(
              exploration.states['State 2'], 'TextInput')
  
          # Other miscellaneous requirements for validation.
          exploration.title = 'Title'
          exploration.category = 'Category'
          exploration.objective = 'Objective'
  
          # The exploration should NOT be terminable even though it has a state
          # called 'END' and everything else is connected to it.
          with self.assertRaisesRegexp(
              Exception,
              'This state does not have any interaction specified.'):
              exploration.validate(strict=True)
  
          # Renaming the node to something other than 'END' and giving it an
          # EndExploration is enough to validate it, though it cannot have a
          # default outcome or answer groups.
          exploration.rename_state('END', 'AnotherEnd')
          another_end_state = exploration.states['AnotherEnd']
          self.set_interaction_for_state(another_end_state, 'EndExploration')
          another_end_state.update_interaction_default_outcome(None)
          exploration.validate(strict=True)
  
          # Name it back for final tests.
          exploration.rename_state('AnotherEnd', 'END')
  
          # Should be able to successfully delete it.
          exploration.delete_state('END')
          self.assertNotIn('END', exploration.states)
  
      def test_update_solicit_answer_details(self):
          """Test updating solicit_answer_details."""
          state = state_domain.State.create_default_state('state_1')
          self.assertEqual(state.solicit_answer_details, False)
          state.update_solicit_answer_details(True)
          self.assertEqual(state.solicit_answer_details, True)
  
      def test_update_solicit_answer_details_with_non_bool_fails(self):
          """Test updating solicit_answer_details with non bool value."""
          exploration = exp_domain.Exploration.create_default_exploration('eid')
          init_state = exploration.states[exploration.init_state_name]
          self.assertEqual(init_state.solicit_answer_details, False)
          with self.assertRaisesRegexp(Exception, (
              'Expected solicit_answer_details to be a boolean, received')):
              init_state.update_solicit_answer_details('abc')
          init_state = exploration.states[exploration.init_state_name]
          self.assertEqual(init_state.solicit_answer_details, False)
  
      def test_update_linked_skill_id(self):
          """Test updating linked_skill_id."""
          state = state_domain.State.create_default_state('state_1')
          self.assertEqual(state.linked_skill_id, None)
          state.update_linked_skill_id('string_2')
          self.assertEqual(state.linked_skill_id, 'string_2')
  
      def test_update_card_is_checkpoint(self):
          """Test update card_is_checkpoint."""
          state = state_domain.State.create_default_state('state_1')
          self.assertEqual(state.card_is_checkpoint, False)
          state.update_card_is_checkpoint(True)
          self.assertEqual(state.card_is_checkpoint, True)
  
      def test_update_card_is_checkpoint_with_non_bool_fails(self):
          """Test updating card_is_checkpoint with non bool value."""
          exploration = exp_domain.Exploration.create_default_exploration('eid')
          init_state = exploration.states[exploration.init_state_name]
          self.assertEqual(init_state.card_is_checkpoint, True)
          with self.assertRaisesRegexp(Exception, (
              'Expected card_is_checkpoint to be a boolean, received')):
              init_state.update_card_is_checkpoint('abc')
          init_state = exploration.states[exploration.init_state_name]
          self.assertEqual(init_state.card_is_checkpoint, True)
  
      def test_convert_html_fields_in_state_with_drag_and_drop_interaction(self):
          """Test the method for converting all the HTML in a state having
          DragAndDropSortInput interaction.
          """
          html_with_old_math_schema = (
              '<p>Value</p><oppia-noninteractive-math raw_latex-with-value="&a'
              'mp;quot;+,-,-,+&amp;quot;"></oppia-noninteractive-math>')
          html_with_new_math_schema = (
              '<p>Value</p><oppia-noninteractive-math math_content-with-value='
              '"{&amp;quot;raw_latex&amp;quot;: &amp;quot;+,-,-,+&amp;quot;, &'
              'amp;quot;svg_filename&amp;quot;: &amp;quot;&amp;quot;}"></oppia'
              '-noninteractive-math>')
          written_translations_dict_with_old_math_schema = {
              'translations_mapping': {
                  'content1': {
                      'en': {
                          'data_format': 'html',
                          'translation': html_with_old_math_schema,
                          'needs_update': True
                      },
                      'hi': {
                          'data_format': 'html',
                          'translation': 'Hey!',
                          'needs_update': False
                      }
                  },
                  'feedback_1': {
                      'hi': {
                          'data_format': 'html',
                          'translation': html_with_old_math_schema,
                          'needs_update': False
                      },
                      'en': {
                          'data_format': 'html',
                          'translation': 'hello!',
                          'needs_update': False
                      }
                  }
              }
          }
          written_translations_dict_with_new_math_schema = {
              'translations_mapping': {
                  'content1': {
                      'en': {
                          'data_format': 'html',
                          'translation': html_with_new_math_schema,
                          'needs_update': True
                      },
                      'hi': {
                          'data_format': 'html',
                          'translation': 'Hey!',
                          'needs_update': False
                      }
                  },
                  'feedback_1': {
                      'hi': {
                          'data_format': 'html',
                          'translation': html_with_new_math_schema,
                          'needs_update': False
                      },
                      'en': {
                          'data_format': 'html',
                          'translation': 'hello!',
                          'needs_update': False
                      }
                  }
              }
          }
  
          answer_group_dict_with_old_math_schema = {
              'outcome': {
                  'dest': 'Introduction',
                  'feedback': {
                      'content_id': 'feedback_1',
                      'html': '<p>Feedback</p>'
                  },
                  'labelled_as_correct': False,
                  'param_changes': [],
                  'refresher_exploration_id': None,
                  'missing_prerequisite_skill_id': None
              },
              'rule_specs': [{
                  'inputs': {
                      'x': [[html_with_old_math_schema]]
                  },
                  'rule_type': 'IsEqualToOrdering'
              }, {
                  'rule_type': 'HasElementXAtPositionY',
                  'inputs': {
                      'x': html_with_old_math_schema,
                      'y': 2
                  }
              }, {
                  'rule_type': 'IsEqualToOrdering',
                  'inputs': {
                      'x': [[html_with_old_math_schema]]
                  }
              }, {
                  'rule_type': 'HasElementXBeforeElementY',
                  'inputs': {
                      'x': html_with_old_math_schema,
                      'y': html_with_old_math_schema
                  }
              }, {
                  'rule_type': 'IsEqualToOrderingWithOneItemAtIncorrectPosition',
                  'inputs': {
                      'x': [[html_with_old_math_schema]]
                  }
              }],
              'training_data': [],
              'tagged_skill_misconception_id': None
          }
          answer_group_dict_with_new_math_schema = {
              'outcome': {
                  'dest': 'Introduction',
                  'feedback': {
                      'content_id': 'feedback_1',
                      'html': '<p>Feedback</p>'
                  },
                  'labelled_as_correct': False,
                  'param_changes': [],
                  'refresher_exploration_id': None,
                  'missing_prerequisite_skill_id': None
              },
              'rule_specs': [{
                  'inputs': {
                      'x': [[html_with_new_math_schema]]
                  },
                  'rule_type': 'IsEqualToOrdering'
              }, {
                  'rule_type': 'HasElementXAtPositionY',
                  'inputs': {
                      'x': html_with_new_math_schema,
                      'y': 2
                  }
              }, {
                  'rule_type': 'IsEqualToOrdering',
                  'inputs': {
                      'x': [[html_with_new_math_schema]]
                  }
              }, {
                  'rule_type': 'HasElementXBeforeElementY',
                  'inputs': {
                      'x': html_with_new_math_schema,
                      'y': html_with_new_math_schema
                  }
              }, {
                  'rule_type': 'IsEqualToOrderingWithOneItemAtIncorrectPosition',
                  'inputs': {
                      'x': [[html_with_new_math_schema]]
                  }
              }],
              'training_data': [],
              'tagged_skill_misconception_id': None
          }
          state_dict_with_old_math_schema = {
              'content': {
                  'content_id': 'content', 'html': 'Hello!'
              },
              'param_changes': [],
              'content_ids_to_audio_translations': {'content': {}},
              'solicit_answer_details': False,
              'card_is_checkpoint': False,
              'linked_skill_id': None,
              'classifier_model_id': None,
              'interaction': {
                  'answer_groups': [answer_group_dict_with_old_math_schema],
                  'default_outcome': {
                      'param_changes': [],
                      'feedback': {
                          'content_id': 'default_outcome',
                          'html': (
                              '<p><oppia-noninteractive-image filepath'
                              '-with-value="&amp;quot;random.png&amp;'
                              'quot;"></oppia-noninteractive-image>'
                              'Hello this is test case to check '
                              'image tag inside p tag</p>'
                          )
                      },
                      'dest': 'Introduction',
                      'refresher_exploration_id': None,
                      'missing_prerequisite_skill_id': None,
                      'labelled_as_correct': False
                  },
                  'customization_args': {
                      'choices': {
                          'value': [{
                              'content_id': 'ca_choices_0',
                              'html': html_with_old_math_schema
                          }, {
                              'content_id': 'ca_choices_1',
                              'html': '<p>2</p>'
                          }, {
                              'content_id': 'ca_choices_2',
                              'html': '<p>3</p>'
                          }, {
                              'content_id': 'ca_choices_3',
                              'html': '<p>4</p>'
                          }]
                      },
                      'allowMultipleItemsInSamePosition': {'value': True}
                  },
                  'confirmed_unclassified_answers': [],
                  'id': 'DragAndDropSortInput',
                  'hints': [
                      {
                          'hint_content': {
                              'content_id': 'hint_1',
                              'html': html_with_old_math_schema
                          }
                      },
                      {
                          'hint_content': {
                              'content_id': 'hint_2',
                              'html': html_with_old_math_schema
                          }
                      }
                  ],
                  'solution': {
                      'answer_is_exclusive': True,
                      'correct_answer': [
                          [html_with_old_math_schema],
                          ['<p>2</p>'],
                          ['<p>3</p>'],
                          ['<p>4</p>']
                      ],
                      'explanation': {
                          'content_id': 'solution',
                          'html': '<p>This is solution for state1</p>'
                      }
                  }
  
              },
              'written_translations': (
                  written_translations_dict_with_old_math_schema)
          }
  
          state_dict_with_new_math_schema = {
              'content': {
                  'content_id': 'content', 'html': 'Hello!'
              },
              'param_changes': [],
              'content_ids_to_audio_translations': {'content': {}},
              'solicit_answer_details': False,
              'card_is_checkpoint': False,
              'linked_skill_id': None,
              'classifier_model_id': None,
              'interaction': {
                  'answer_groups': [answer_group_dict_with_new_math_schema],
                  'default_outcome': {
                      'param_changes': [],
                      'feedback': {
                          'content_id': 'default_outcome',
                          'html': (
                              '<p><oppia-noninteractive-image filepath'
                              '-with-value="&amp;quot;random.png&amp;'
                              'quot;"></oppia-noninteractive-image>'
                              'Hello this is test case to check '
                              'image tag inside p tag</p>'
                          )
                      },
                      'dest': 'Introduction',
                      'refresher_exploration_id': None,
                      'missing_prerequisite_skill_id': None,
                      'labelled_as_correct': False
                  },
                  'customization_args': {
                      'choices': {
                          'value': [{
                              'content_id': 'ca_choices_0',
                              'html': html_with_new_math_schema
                          }, {
                              'content_id': 'ca_choices_1',
                              'html': '<p>2</p>'
                          }, {
                              'content_id': 'ca_choices_2',
                              'html': '<p>3</p>'
                          }, {
                              'content_id': 'ca_choices_3',
                              'html': '<p>4</p>'
                          }]
                      },
                      'allowMultipleItemsInSamePosition': {'value': True}
                  },
                  'confirmed_unclassified_answers': [],
                  'id': 'DragAndDropSortInput',
                  'hints': [
                      {
                          'hint_content': {
                              'content_id': 'hint_1',
                              'html': html_with_new_math_schema
                          }
                      },
                      {
                          'hint_content': {
                              'content_id': 'hint_2',
                              'html': html_with_new_math_schema
                          }
                      }
                  ],
                  'solution': {
                      'answer_is_exclusive': True,
                      'correct_answer': [
                          [html_with_new_math_schema],
                          ['<p>2</p>'],
                          ['<p>3</p>'],
                          ['<p>4</p>']
                      ],
                      'explanation': {
                          'content_id': 'solution',
                          'html': '<p>This is solution for state1</p>'
                      }
                  }
  
              },
              'written_translations': (
                  written_translations_dict_with_new_math_schema)
          }
          self.assertEqual(
              state_domain.State.convert_html_fields_in_state(
                  state_dict_with_old_math_schema,
                  html_validation_service.
                  add_math_content_to_math_rte_components,
                  state_uses_old_rule_template_schema=True),
              state_dict_with_new_math_schema)
  
      def test_convert_html_fields_in_state_with_item_selection_interaction(self):
          """Test the method for converting all the HTML in a state having
          ItemSelection interaction.
          """
          html_with_old_math_schema = (
              '<p>Value</p><oppia-noninteractive-math raw_latex-with-value="&a'
              'mp;quot;+,-,-,+&amp;quot;"></oppia-noninteractive-math>')
          html_with_new_math_schema = (
              '<p>Value</p><oppia-noninteractive-math math_content-with-value='
              '"{&amp;quot;raw_latex&amp;quot;: &amp;quot;+,-,-,+&amp;quot;, &'
              'amp;quot;svg_filename&amp;quot;: &amp;quot;&amp;quot;}"></oppia'
              '-noninteractive-math>')
          answer_group_with_old_math_schema = [{
              'rule_specs': [{
                  'rule_type': 'Equals',
                  'inputs': {
                      'x': [html_with_old_math_schema]
                  }
              }, {
                  'rule_type': 'ContainsAtLeastOneOf',
                  'inputs': {
                      'x': [html_with_old_math_schema]
                  }
              }, {
                  'rule_type': 'IsProperSubsetOf',
                  'inputs': {
                      'x': [html_with_old_math_schema]
                  }
              }, {
                  'rule_type': 'DoesNotContainAtLeastOneOf',
                  'inputs': {
                      'x': [html_with_old_math_schema]
                  }
              }],
              'outcome': {
                  'dest': 'Introduction',
                  'feedback': {
                      'content_id': 'feedback',
                      'html': html_with_old_math_schema
                  },
                  'param_changes': [],
                  'labelled_as_correct': False,
                  'refresher_exploration_id': None,
                  'missing_prerequisite_skill_id': None
              },
              'training_data': [],
              'tagged_skill_misconception_id': None
          }]
  
          answer_group_with_new_math_schema = [{
              'rule_specs': [{
                  'rule_type': 'Equals',
                  'inputs': {
                      'x': [html_with_new_math_schema]
                  }
              }, {
                  'rule_type': 'ContainsAtLeastOneOf',
                  'inputs': {
                      'x': [html_with_new_math_schema]
                  }
              }, {
                  'rule_type': 'IsProperSubsetOf',
                  'inputs': {
                      'x': [html_with_new_math_schema]
                  }
              }, {
                  'rule_type': 'DoesNotContainAtLeastOneOf',
                  'inputs': {
                      'x': [html_with_new_math_schema]
                  }
              }],
              'outcome': {
                  'dest': 'Introduction',
                  'feedback': {
                      'content_id': 'feedback',
                      'html': html_with_new_math_schema
                  },
                  'param_changes': [],
                  'labelled_as_correct': False,
                  'refresher_exploration_id': None,
                  'missing_prerequisite_skill_id': None
              },
              'training_data': [],
              'tagged_skill_misconception_id': None
          }]
  
          state_dict_with_old_math_schema = {
              'content': {
                  'content_id': 'content', 'html': 'Hello!'
              },
              'param_changes': [],
              'content_ids_to_audio_translations': {'content': {}},
              'solicit_answer_details': False,
              'card_is_checkpoint': False,
              'linked_skill_id': None,
              'classifier_model_id': None,
              'interaction': {
                  'solution': {
                      'answer_is_exclusive': True,
                      'correct_answer': [
                          html_with_old_math_schema,
                          '<p>state customization arg html 2</p>',
                          '<p>state customization arg html 3</p>',
                          '<p>state customization arg html 4</p>'
                      ],
                      'explanation': {
                          'content_id': 'solution',
                          'html': '<p>This is solution for state1</p>'
                      }
                  },
                  'answer_groups': answer_group_with_old_math_schema,
                  'default_outcome': {
                      'param_changes': [],
                      'feedback': {
                          'content_id': 'default_outcome',
                          'html': (
                              '<p><oppia-noninteractive-image filepath'
                              '-with-value="&amp;quot;random.png&amp;'
                              'quot;"></oppia-noninteractive-image>'
                              'Hello this is test case to check '
                              'image tag inside p tag</p>'
                          )
                      },
                      'dest': 'Introduction',
                      'refresher_exploration_id': None,
                      'missing_prerequisite_skill_id': None,
                      'labelled_as_correct': False
                  },
                  'customization_args': {
                      'maxAllowableSelectionCount': {
                          'value': 1
                      },
                      'minAllowableSelectionCount': {
                          'value': 1
                      },
                      'choices': {
                          'value': [{
                              'content_id': 'ca_choices_0',
                              'html': '<p>init_state customization arg html 1</p>'
                          }, {
                              'content_id': 'ca_choices_1',
                              'html': html_with_old_math_schema
                          }, {
                              'content_id': 'ca_choices_2',
                              'html': '<p>init_state customization arg html 3</p>'
                          }, {
                              'content_id': 'ca_choices_3',
                              'html': '<p>init_state customization arg html 4</p>'
                          }]
                      }
                  },
                  'confirmed_unclassified_answers': [],
                  'id': 'ItemSelectionInput',
                  'hints': []
              }
          }
  
          state_dict_with_new_math_schema = {
              'content': {
                  'content_id': 'content', 'html': 'Hello!'
              },
              'param_changes': [],
              'content_ids_to_audio_translations': {'content': {}},
              'solicit_answer_details': False,
              'card_is_checkpoint': False,
              'linked_skill_id': None,
              'classifier_model_id': None,
              'interaction': {
                  'solution': {
                      'answer_is_exclusive': True,
                      'correct_answer': [
                          html_with_new_math_schema,
                          '<p>state customization arg html 2</p>',
                          '<p>state customization arg html 3</p>',
                          '<p>state customization arg html 4</p>'
                      ],
                      'explanation': {
                          'content_id': 'solution',
                          'html': '<p>This is solution for state1</p>'
                      }
                  },
                  'answer_groups': answer_group_with_new_math_schema,
                  'default_outcome': {
                      'param_changes': [],
                      'feedback': {
                          'content_id': 'default_outcome',
                          'html': (
                              '<p><oppia-noninteractive-image filepath'
                              '-with-value="&amp;quot;random.png&amp;'
                              'quot;"></oppia-noninteractive-image>'
                              'Hello this is test case to check '
                              'image tag inside p tag</p>'
                          )
                      },
                      'dest': 'Introduction',
                      'refresher_exploration_id': None,
                      'missing_prerequisite_skill_id': None,
                      'labelled_as_correct': False
                  },
                  'customization_args': {
                      'maxAllowableSelectionCount': {
                          'value': 1
                      },
                      'minAllowableSelectionCount': {
                          'value': 1
                      },
                      'choices': {
                          'value': [{
                              'content_id': 'ca_choices_0',
                              'html': '<p>init_state customization arg html 1</p>'
                          }, {
                              'content_id': 'ca_choices_1',
                              'html': html_with_new_math_schema
                          }, {
                              'content_id': 'ca_choices_2',
                              'html': '<p>init_state customization arg html 3</p>'
                          }, {
                              'content_id': 'ca_choices_3',
                              'html': '<p>init_state customization arg html 4</p>'
                          }]
                      }
                  },
                  'confirmed_unclassified_answers': [],
                  'id': 'ItemSelectionInput',
                  'hints': []
              }
          }
          interaction_registry.Registry.get_all_specs_for_state_schema_version(
              41)['ItemSelectionInput']['can_have_solution'] = True
  
          self.assertEqual(
              state_domain.State.convert_html_fields_in_state(
                  state_dict_with_old_math_schema,
                  html_validation_service.
                  add_math_content_to_math_rte_components,
                  state_uses_old_rule_template_schema=True),
              state_dict_with_new_math_schema)
  
      def test_convert_html_fields_in_state_with_text_input_interaction(self):
          """Test the method for converting all the HTML in a state having
          TextInput interaction.
          """
          html_with_old_math_schema = (
              '<p>Value</p><oppia-noninteractive-math raw_latex-with-value="&a'
              'mp;quot;+,-,-,+&amp;quot;"></oppia-noninteractive-math>')
          html_with_new_math_schema = (
              '<p>Value</p><oppia-noninteractive-math math_content-with-value='
              '"{&amp;quot;raw_latex&amp;quot;: &amp;quot;+,-,-,+&amp;quot;, &'
              'amp;quot;svg_filename&amp;quot;: &amp;quot;&amp;quot;}"></oppia'
              '-noninteractive-math>')
          answer_group_with_old_math_schema = {
              'outcome': {
                  'dest': 'Introduction',
                  'feedback': {
                      'content_id': 'feedback_1',
                      'html': html_with_old_math_schema
                  },
                  'labelled_as_correct': False,
                  'param_changes': [],
                  'refresher_exploration_id': None,
                  'missing_prerequisite_skill_id': None
              },
              'rule_specs': [{
                  'inputs': {
                      'x': 'Test'
                  },
                  'rule_type': 'Equals'
              }],
              'training_data': [],
              'tagged_skill_misconception_id': None
          }
          answer_group_with_new_math_schema = {
              'outcome': {
                  'dest': 'Introduction',
                  'feedback': {
                      'content_id': 'feedback_1',
                      'html': html_with_new_math_schema
                  },
                  'labelled_as_correct': False,
                  'param_changes': [],
                  'refresher_exploration_id': None,
                  'missing_prerequisite_skill_id': None
              },
              'rule_specs': [{
                  'inputs': {
                      'x': 'Test'
                  },
                  'rule_type': 'Equals'
              }],
              'training_data': [],
              'tagged_skill_misconception_id': None
          }
  
          state_dict_with_old_math_schema = {
              'content': {
                  'content_id': 'content', 'html': html_with_old_math_schema
              },
              'param_changes': [],
              'content_ids_to_audio_translations': {'content': {}},
              'solicit_answer_details': False,
              'card_is_checkpoint': False,
              'linked_skill_id': None,
              'classifier_model_id': None,
              'interaction': {
                  'solution': {
                      'answer_is_exclusive': True,
                      'correct_answer': 'Answer1',
                      'explanation': {
                          'content_id': 'solution',
                          'html': html_with_old_math_schema
                      }
                  },
                  'answer_groups': [answer_group_with_old_math_schema],
                  'default_outcome': {
                      'param_changes': [],
                      'feedback': {
                          'content_id': 'default_outcome',
                          'html': html_with_old_math_schema
                      },
                      'dest': 'Introduction',
                      'refresher_exploration_id': None,
                      'missing_prerequisite_skill_id': None,
                      'labelled_as_correct': False
                  },
                  'customization_args': {
                      'rows': {
                          'value': 1
                      },
                      'placeholder': {
                          'value': {
                              'content_id': 'ca_placeholder_0',
                              'unicode_str': ''
                          }
                      }
                  },
                  'confirmed_unclassified_answers': [],
                  'id': 'TextInput',
                  'hints': [
                      {
                          'hint_content': {
                              'content_id': 'hint_1',
                              'html': html_with_old_math_schema
                          }
                      },
                      {
                          'hint_content': {
                              'content_id': 'hint_2',
                              'html': html_with_old_math_schema
                          }
                      }]
              }
          }
  
          state_dict_with_new_math_schema = {
              'content': {
                  'content_id': 'content', 'html': html_with_new_math_schema
              },
              'param_changes': [],
              'content_ids_to_audio_translations': {'content': {}},
              'solicit_answer_details': False,
              'card_is_checkpoint': False,
              'linked_skill_id': None,
              'classifier_model_id': None,
              'interaction': {
                  'solution': {
                      'answer_is_exclusive': True,
                      'correct_answer': 'Answer1',
                      'explanation': {
                          'content_id': 'solution',
                          'html': html_with_new_math_schema
                      }
                  },
                  'answer_groups': [answer_group_with_new_math_schema],
                  'default_outcome': {
                      'param_changes': [],
                      'feedback': {
                          'content_id': 'default_outcome',
                          'html': html_with_new_math_schema
                      },
                      'dest': 'Introduction',
                      'refresher_exploration_id': None,
                      'missing_prerequisite_skill_id': None,
                      'labelled_as_correct': False
                  },
                  'customization_args': {
                      'rows': {
                          'value': 1
                      },
                      'placeholder': {
                          'value': {
                              'content_id': 'ca_placeholder_0',
                              'unicode_str': ''
                          }
                      }
                  },
                  'confirmed_unclassified_answers': [],
                  'id': 'TextInput',
                  'hints': [
                      {
                          'hint_content': {
                              'content_id': 'hint_1',
                              'html': html_with_new_math_schema
                          }
                      },
                      {
                          'hint_content': {
                              'content_id': 'hint_2',
                              'html': html_with_new_math_schema
                          }
                      }]
              }
          }
          self.assertEqual(
              state_domain.State.convert_html_fields_in_state(
                  state_dict_with_old_math_schema,
                  html_validation_service.
                  add_math_content_to_math_rte_components),
              state_dict_with_new_math_schema)
  
      def test_convert_html_fields_in_state_with_math_expression_input(self):
          """Test the method for converting all the HTML in a state having
          MathExpressionInput interaction.
          """
          html_with_old_math_schema = (
              '<p>Value</p><oppia-noninteractive-math raw_latex-with-value="&a'
              'mp;quot;+,-,-,+&amp;quot;"></oppia-noninteractive-math>')
          html_with_new_math_schema = (
              '<p>Value</p><oppia-noninteractive-math math_content-with-value='
              '"{&amp;quot;raw_latex&amp;quot;: &amp;quot;+,-,-,+&amp;quot;, &'
              'amp;quot;svg_filename&amp;quot;: &amp;quot;&amp;quot;}"></oppia'
              '-noninteractive-math>')
          answer_group_with_old_math_schema = {
              'outcome': {
                  'dest': 'Introduction',
                  'feedback': {
                      'content_id': 'feedback_1',
                      'html': html_with_old_math_schema
                  },
                  'labelled_as_correct': False,
                  'param_changes': [],
                  'refresher_exploration_id': None,
                  'missing_prerequisite_skill_id': None
              },
              'rule_specs': [{
                  'inputs': {
                      'x': 'Test'
                  },
                  'rule_type': 'Equals'
              }],
              'training_data': [],
              'tagged_skill_misconception_id': None
          }
          answer_group_with_new_math_schema = {
              'outcome': {
                  'dest': 'Introduction',
                  'feedback': {
                      'content_id': 'feedback_1',
                      'html': html_with_new_math_schema
                  },
                  'labelled_as_correct': False,
                  'param_changes': [],
                  'refresher_exploration_id': None,
                  'missing_prerequisite_skill_id': None
              },
              'rule_specs': [{
                  'inputs': {
                      'x': 'Test'
                  },
                  'rule_type': 'Equals'
              }],
              'training_data': [],
              'tagged_skill_misconception_id': None
          }
  
          state_dict_with_old_math_schema = {
              'content': {
                  'content_id': 'content', 'html': html_with_old_math_schema
              },
              'param_changes': [],
              'content_ids_to_audio_translations': {'content': {}},
              'solicit_answer_details': False,
              'card_is_checkpoint': False,
              'linked_skill_id': None,
              'classifier_model_id': None,
              'interaction': {
                  'solution': {
                      'answer_is_exclusive': True,
                      'correct_answer': '42',
                      'explanation': {
                          'content_id': 'solution',
                          'html': html_with_old_math_schema
                      }
                  },
                  'answer_groups': [answer_group_with_old_math_schema],
                  'default_outcome': {
                      'param_changes': [],
                      'feedback': {
                          'content_id': 'default_outcome',
                          'html': html_with_old_math_schema
                      },
                      'dest': 'Introduction',
                      'refresher_exploration_id': None,
                      'missing_prerequisite_skill_id': None,
                      'labelled_as_correct': False
                  },
                  'customization_args': {},
                  'confirmed_unclassified_answers': [],
                  'id': 'MathExpressionInput',
                  'hints': [
                      {
                          'hint_content': {
                              'content_id': 'hint_1',
                              'html': html_with_old_math_schema
                          }
                      },
                      {
                          'hint_content': {
                              'content_id': 'hint_2',
                              'html': html_with_old_math_schema
                          }
                      }]
              }
          }
  
          state_dict_with_new_math_schema = {
              'content': {
                  'content_id': 'content', 'html': html_with_new_math_schema
              },
              'param_changes': [],
              'content_ids_to_audio_translations': {'content': {}},
              'solicit_answer_details': False,
              'card_is_checkpoint': False,
              'linked_skill_id': None,
              'classifier_model_id': None,
              'interaction': {
                  'solution': {
                      'answer_is_exclusive': True,
                      'correct_answer': '42',
                      'explanation': {
                          'content_id': 'solution',
                          'html': html_with_new_math_schema
                      }
                  },
                  'answer_groups': [answer_group_with_new_math_schema],
                  'default_outcome': {
                      'param_changes': [],
                      'feedback': {
                          'content_id': 'default_outcome',
                          'html': html_with_new_math_schema
                      },
                      'dest': 'Introduction',
                      'refresher_exploration_id': None,
                      'missing_prerequisite_skill_id': None,
                      'labelled_as_correct': False
                  },
                  'customization_args': {},
                  'confirmed_unclassified_answers': [],
                  'id': 'MathExpressionInput',
                  'hints': [
                      {
                          'hint_content': {
                              'content_id': 'hint_1',
                              'html': html_with_new_math_schema
                          }
                      },
                      {
                          'hint_content': {
                              'content_id': 'hint_2',
                              'html': html_with_new_math_schema
                          }
                      }]
              }
          }
          self.assertEqual(
              state_domain.State.convert_html_fields_in_state(
                  state_dict_with_old_math_schema,
                  html_validation_service.
                  add_math_content_to_math_rte_components),
              state_dict_with_new_math_schema)
  
      def test_convert_html_fields_in_state_with_old_written_translations(self):
          """Test the method for converting all the HTML in a state having
          written_translations in the old format. This is needed for converting
          older snapshots (prior to state schema version 35) properly.
  
          TODO(#11950): Remove this test once old schema migration functions are
          deleted.
          """
          html_with_old_math_schema = (
              '<p>Value</p><oppia-noninteractive-math raw_latex-with-value="&a'
              'mp;quot;+,-,-,+&amp;quot;"></oppia-noninteractive-math>')
          html_with_new_math_schema = (
              '<p>Value</p><oppia-noninteractive-math math_content-with-value='
              '"{&amp;quot;raw_latex&amp;quot;: &amp;quot;+,-,-,+&amp;quot;, &'
              'amp;quot;svg_filename&amp;quot;: &amp;quot;&amp;quot;}"></oppia'
              '-noninteractive-math>')
          written_translations_dict_with_old_math_schema_and_old_format = {
              'translations_mapping': {
                  'content1': {
                      'en': {
                          'html': html_with_old_math_schema,
                          'needs_update': True
                      },
                      'hi': {
                          'html': 'Hey!',
                          'needs_update': False
                      }
                  },
                  'feedback_1': {
                      'hi': {
                          'html': html_with_old_math_schema,
                          'needs_update': False
                      },
                      'en': {
                          'html': 'hello!',
                          'needs_update': False
                      }
                  }
              }
          }
          written_translations_dict_with_new_math_schema_and_old_format = {
              'translations_mapping': {
                  'content1': {
                      'en': {
                          'html': html_with_new_math_schema,
                          'needs_update': True
                      },
                      'hi': {
                          'html': 'Hey!',
                          'needs_update': False
                      }
                  },
                  'feedback_1': {
                      'hi': {
                          'html': html_with_new_math_schema,
                          'needs_update': False
                      },
                      'en': {
                          'html': 'hello!',
                          'needs_update': False
                      }
                  }
              }
          }
  
          answer_group_dict_with_old_math_schema = {
              'outcome': {
                  'dest': 'Introduction',
                  'feedback': {
                      'content_id': 'feedback_1',
                      'html': '<p>Feedback</p>'
                  },
                  'labelled_as_correct': False,
                  'param_changes': [],
                  'refresher_exploration_id': None,
                  'missing_prerequisite_skill_id': None
              },
              'rule_specs': [{
                  'inputs': {
                      'x': [[html_with_old_math_schema]]
                  },
                  'rule_type': 'IsEqualToOrdering'
              }],
              'training_data': [],
              'tagged_skill_misconception_id': None
          }
          answer_group_dict_with_new_math_schema = {
              'outcome': {
                  'dest': 'Introduction',
                  'feedback': {
                      'content_id': 'feedback_1',
                      'html': '<p>Feedback</p>'
                  },
                  'labelled_as_correct': False,
                  'param_changes': [],
                  'refresher_exploration_id': None,
                  'missing_prerequisite_skill_id': None
              },
              'rule_specs': [{
                  'inputs': {
                      'x': [[html_with_new_math_schema]]
                  },
                  'rule_type': 'IsEqualToOrdering'
              }],
              'training_data': [],
              'tagged_skill_misconception_id': None
          }
          state_dict_with_old_math_schema = {
              'content': {
                  'content_id': 'content', 'html': 'Hello!'
              },
              'param_changes': [],
              'content_ids_to_audio_translations': {'content': {}},
              'solicit_answer_details': False,
              'card_is_checkpoint': False,
              'linked_skill_id': None,
              'classifier_model_id': None,
              'interaction': {
                  'answer_groups': [answer_group_dict_with_old_math_schema],
                  'default_outcome': {
                      'param_changes': [],
                      'feedback': {
                          'content_id': 'default_outcome',
                          'html': (
                              '<p><oppia-noninteractive-image filepath'
                              '-with-value="&amp;quot;random.png&amp;'
                              'quot;"></oppia-noninteractive-image>'
                              'Hello this is test case to check '
                              'image tag inside p tag</p>'
                          )
                      },
                      'dest': 'Introduction',
                      'refresher_exploration_id': None,
                      'missing_prerequisite_skill_id': None,
                      'labelled_as_correct': False
                  },
                  'customization_args': {
                      'choices': {
                          'value': [{
                              'content_id': 'ca_choices_0',
                              'html': html_with_old_math_schema
                          }, {
                              'content_id': 'ca_choices_1',
                              'html': '<p>2</p>'
                          }, {
                              'content_id': 'ca_choices_2',
                              'html': '<p>3</p>'
                          }, {
                              'content_id': 'ca_choices_3',
                              'html': '<p>4</p>'
                          }]
                      },
                      'allowMultipleItemsInSamePosition': {'value': True}
                  },
                  'confirmed_unclassified_answers': [],
                  'id': 'DragAndDropSortInput',
                  'hints': [
                      {
                          'hint_content': {
                              'content_id': 'hint_1',
                              'html': html_with_old_math_schema
                          }
                      },
                      {
                          'hint_content': {
                              'content_id': 'hint_2',
                              'html': html_with_old_math_schema
                          }
                      }
                  ],
                  'solution': {
                      'answer_is_exclusive': True,
                      'correct_answer': [
                          [html_with_old_math_schema],
                          ['<p>2</p>'],
                          ['<p>3</p>'],
                          ['<p>4</p>']
                      ],
                      'explanation': {
                          'content_id': 'solution',
                          'html': '<p>This is solution for state1</p>'
                      }
                  }
  
              },
              'written_translations': (
                  written_translations_dict_with_old_math_schema_and_old_format)
          }
  
          state_dict_with_new_math_schema = {
              'content': {
                  'content_id': 'content', 'html': 'Hello!'
              },
              'param_changes': [],
              'content_ids_to_audio_translations': {'content': {}},
              'solicit_answer_details': False,
              'card_is_checkpoint': False,
              'linked_skill_id': None,
              'classifier_model_id': None,
              'interaction': {
                  'answer_groups': [answer_group_dict_with_new_math_schema],
                  'default_outcome': {
                      'param_changes': [],
                      'feedback': {
                          'content_id': 'default_outcome',
                          'html': (
                              '<p><oppia-noninteractive-image filepath'
                              '-with-value="&amp;quot;random.png&amp;'
                              'quot;"></oppia-noninteractive-image>'
                              'Hello this is test case to check '
                              'image tag inside p tag</p>'
                          )
                      },
                      'dest': 'Introduction',
                      'refresher_exploration_id': None,
                      'missing_prerequisite_skill_id': None,
                      'labelled_as_correct': False
                  },
                  'customization_args': {
                      'choices': {
                          'value': [{
                              'content_id': 'ca_choices_0',
                              'html': html_with_new_math_schema
                          }, {
                              'content_id': 'ca_choices_1',
                              'html': '<p>2</p>'
                          }, {
                              'content_id': 'ca_choices_2',
                              'html': '<p>3</p>'
                          }, {
                              'content_id': 'ca_choices_3',
                              'html': '<p>4</p>'
                          }]
                      },
                      'allowMultipleItemsInSamePosition': {'value': True}
                  },
                  'confirmed_unclassified_answers': [],
                  'id': 'DragAndDropSortInput',
                  'hints': [
                      {
                          'hint_content': {
                              'content_id': 'hint_1',
                              'html': html_with_new_math_schema
                          }
                      },
                      {
                          'hint_content': {
                              'content_id': 'hint_2',
                              'html': html_with_new_math_schema
                          }
                      }
                  ],
                  'solution': {
                      'answer_is_exclusive': True,
                      'correct_answer': [
                          [html_with_new_math_schema],
                          ['<p>2</p>'],
                          ['<p>3</p>'],
                          ['<p>4</p>']
                      ],
                      'explanation': {
                          'content_id': 'solution',
                          'html': '<p>This is solution for state1</p>'
                      }
                  }
  
              },
              'written_translations': (
                  written_translations_dict_with_new_math_schema_and_old_format)
          }
          self.assertEqual(
              state_domain.State.convert_html_fields_in_state(
                  state_dict_with_old_math_schema,
                  html_validation_service.
                  add_math_content_to_math_rte_components,
                  state_uses_old_rule_template_schema=True),
              state_dict_with_new_math_schema)
  
      def test_convert_html_fields_in_state_having_rule_spec_with_invalid_format(
              self):
          """Test the method for converting the HTML in a state
          when the rule_spec has invalid html format.
          """
          html_with_old_math_schema = (
              '<p>Value</p><oppia-noninteractive-math raw_latex-with-value="&a'
              'mp;quot;+,-,-,+&amp;quot;"></oppia-noninteractive-math>')
          answer_group_with_old_math_schema = [{
              'rule_specs': [{
                  'rule_type': 'Equals',
                  'inputs': {
                      'x': [html_with_old_math_schema]
                  }
              }, {
                  'rule_type': 'ContainsAtLeastOneOf',
                  'inputs': {
                      'x': [html_with_old_math_schema]
                  }
              }],
              'outcome': {
                  'dest': 'Introduction',
                  'feedback': {
                      'content_id': 'feedback',
                      'html': html_with_old_math_schema
                  },
                  'param_changes': [],
                  'labelled_as_correct': False,
                  'refresher_exploration_id': None,
                  'missing_prerequisite_skill_id': None
              },
              'training_data': [],
              'tagged_skill_misconception_id': None
          }]
  
          state_dict_with_old_math_schema = {
              'content': {
                  'content_id': 'content', 'html': 'Hello!'
              },
              'param_changes': [],
              'content_ids_to_audio_translations': {'content': {}},
              'solicit_answer_details': False,
              'card_is_checkpoint': False,
              'linked_skill_id': None,
              'classifier_model_id': None,
              'interaction': {
                  'solution': None,
                  'answer_groups': answer_group_with_old_math_schema,
                  'default_outcome': {
                      'param_changes': [],
                      'feedback': {
                          'content_id': 'default_outcome',
                          'html': (
                              '<p><oppia-noninteractive-image filepath'
                              '-with-value="&amp;quot;random.png&amp;'
                              'quot;"></oppia-noninteractive-image>'
                              'Hello this is test case to check '
                              'image tag inside p tag</p>'
                          )
                      },
                      'dest': 'Introduction',
                      'refresher_exploration_id': None,
                      'missing_prerequisite_skill_id': None,
                      'labelled_as_correct': False
                  },
                  'customization_args': {
                      'maxAllowableSelectionCount': {
                          'value': 1
                      },
                      'minAllowableSelectionCount': {
                          'value': 1
                      },
                      'choices': {
                          'value': [
                              '<p>init_state customization arg html 1</p>',
                              html_with_old_math_schema,
                              '<p>init_state customization arg html 3</p>',
                              '<p>init_state customization arg html 4</p>'
                          ]
                      }
                  },
                  'confirmed_unclassified_answers': [],
                  'id': 'ItemSelectionInput',
                  'hints': []
              }
          }
  
          mock_html_field_types_to_rule_specs_dict = copy.deepcopy(
              rules_registry.Registry.get_html_field_types_to_rule_specs(
                  state_schema_version=41))
          for html_type_dict in (
                  mock_html_field_types_to_rule_specs_dict.values()):
              html_type_dict['format'] = 'invalid format'
  
          def mock_get_html_field_types_to_rule_specs(
                  unused_cls, state_schema_version=None): # pylint: disable=unused-argument
              return mock_html_field_types_to_rule_specs_dict
  
          with self.swap(
              rules_registry.Registry, 'get_html_field_types_to_rule_specs',
              classmethod(mock_get_html_field_types_to_rule_specs)):
              with self.assertRaisesRegexp(
                  Exception,
                  'The rule spec does not belong to a valid format.'):
                  state_domain.State.convert_html_fields_in_state(
                      state_dict_with_old_math_schema,
                      html_validation_service.
                      add_math_content_to_math_rte_components,
                      state_uses_old_rule_template_schema=True)
  
      def test_convert_html_fields_in_rule_spec_with_invalid_input_variable(self):
          """Test the method for converting the HTML in a state
          when the rule_spec has invalid input variable.
          """
          html_with_old_math_schema = (
              '<p>Value</p><oppia-noninteractive-math raw_latex-with-value="&a'
              'mp;quot;+,-,-,+&amp;quot;"></oppia-noninteractive-math>')
          answer_group_with_old_math_schema = [{
              'rule_specs': [{
                  'rule_type': 'Equals',
                  'inputs': {
                      'x': [html_with_old_math_schema]
                  }
              }, {
                  'rule_type': 'ContainsAtLeastOneOf',
                  'inputs': {
                      'x': [html_with_old_math_schema]
                  }
              }],
              'outcome': {
                  'dest': 'Introduction',
                  'feedback': {
                      'content_id': 'feedback',
                      'html': html_with_old_math_schema
                  },
                  'param_changes': [],
                  'labelled_as_correct': False,
                  'refresher_exploration_id': None,
                  'missing_prerequisite_skill_id': None
              },
              'training_data': [],
              'tagged_skill_misconception_id': None
          }]
  
          state_dict_with_old_math_schema = {
              'content': {
                  'content_id': 'content', 'html': 'Hello!'
              },
              'param_changes': [],
              'content_ids_to_audio_translations': {'content': {}},
              'solicit_answer_details': False,
              'card_is_checkpoint': False,
              'linked_skill_id': None,
              'classifier_model_id': None,
              'interaction': {
                  'solution': None,
                  'answer_groups': answer_group_with_old_math_schema,
                  'default_outcome': {
                      'param_changes': [],
                      'feedback': {
                          'content_id': 'default_outcome',
                          'html': (
                              '<p><oppia-noninteractive-image filepath'
                              '-with-value="&amp;quot;random.png&amp;'
                              'quot;"></oppia-noninteractive-image>'
                              'Hello this is test case to check '
                              'image tag inside p tag</p>'
                          )
                      },
                      'dest': 'Introduction',
                      'refresher_exploration_id': None,
                      'missing_prerequisite_skill_id': None,
                      'labelled_as_correct': False
                  },
                  'customization_args': {
                      'maxAllowableSelectionCount': {
                          'value': 1
                      },
                      'minAllowableSelectionCount': {
                          'value': 1
                      },
                      'choices': {
                          'value': [
                              '<p>init_state customization arg html 1</p>',
                              html_with_old_math_schema,
                              '<p>init_state customization arg html 3</p>',
                              '<p>init_state customization arg html 4</p>'
                          ]
                      }
                  },
                  'confirmed_unclassified_answers': [],
                  'id': 'ItemSelectionInput',
                  'hints': []
              }
          }
  
          mock_html_field_types_to_rule_specs_dict = copy.deepcopy(
              rules_registry.Registry.get_html_field_types_to_rule_specs(
                  state_schema_version=41))
          for html_type_dict in (
                  mock_html_field_types_to_rule_specs_dict.values()):
              if html_type_dict['interactionId'] == 'ItemSelectionInput':
                  html_type_dict['ruleTypes']['Equals']['htmlInputVariables'] = (
                      ['y'])
  
          def mock_get_html_field_types_to_rule_specs(unused_cls):
              return mock_html_field_types_to_rule_specs_dict
  
          with self.swap(
              rules_registry.Registry, 'get_html_field_types_to_rule_specs',
              classmethod(mock_get_html_field_types_to_rule_specs)
          ):
              with self.assertRaisesRegexp(
                  Exception,
                  'Rule spec should have at least one valid input variable with '
                  'Html in it.'):
                  state_domain.State.convert_html_fields_in_state(
                      state_dict_with_old_math_schema,
                      html_validation_service.
                      add_math_content_to_math_rte_components)
  
      def test_convert_html_fields_in_rule_spec_with_invalid_correct_answer(self):
          """Test the method for converting the HTML in a state when the
          interaction solution has invalid answer type.
          """
          html_with_old_math_schema = (
              '<p>Value</p><oppia-noninteractive-math raw_latex-with-value="&a'
              'mp;quot;+,-,-,+&amp;quot;"></oppia-noninteractive-math>')
  
          state_dict_with_old_math_schema = {
              'content': {
                  'content_id': 'content', 'html': html_with_old_math_schema
              },
              'param_changes': [],
              'content_ids_to_audio_translations': {'content': {}},
              'solicit_answer_details': False,
              'card_is_checkpoint': False,
              'linked_skill_id': None,
              'classifier_model_id': None,
              'interaction': {
                  'solution': {
                      'interaction_id': '',
                      'answer_is_exclusive': True,
                      'correct_answer': 'Answer1',
                      'explanation': {
                          'content_id': 'solution',
                          'html': html_with_old_math_schema
                      }
                  },
                  'answer_groups': [],
                  'default_outcome': {
                      'param_changes': [],
                      'feedback': {
                          'content_id': 'default_outcome',
                          'html': html_with_old_math_schema
                      },
                      'dest': 'Introduction',
                      'refresher_exploration_id': None,
                      'missing_prerequisite_skill_id': None,
                      'labelled_as_correct': False
                  },
                  'customization_args': {
                      'rows': {
                          'value': 1
                      },
                      'placeholder': {
                          'value': ''
                      }
                  },
                  'confirmed_unclassified_answers': [],
                  'id': 'TextInput',
                  'hints': [
                      {
                          'hint_content': {
                              'content_id': 'hint_1',
                              'html': html_with_old_math_schema
                          }
                      },
                      {
                          'hint_content': {
                              'content_id': 'hint_2',
                              'html': html_with_old_math_schema
                          }
                      }
                  ]
              }
          }
  
          mock_html_field_types_to_rule_specs_dict = copy.deepcopy(
              rules_registry.Registry.get_html_field_types_to_rule_specs(
                  state_schema_version=41))
          mock_html_field_types_to_rule_specs_dict['NormalizedString'] = (
              mock_html_field_types_to_rule_specs_dict.pop('SetOfHtmlString'))
  
          def mock_get_html_field_types_to_rule_specs(unused_cls):
              return mock_html_field_types_to_rule_specs_dict
  
          with self.swap(
              rules_registry.Registry, 'get_html_field_types_to_rule_specs',
              classmethod(mock_get_html_field_types_to_rule_specs)
          ):
              with self.assertRaisesRegexp(
                  Exception,
                  'The solution does not have a valid '
                  'correct_answer type.'):
                  state_domain.State.convert_html_fields_in_state(
                      state_dict_with_old_math_schema,
                      html_validation_service.
                      add_math_content_to_math_rte_components)
  
      def test_convert_html_fields_in_state_when_interaction_is_none(self):
          """Test the method for converting all the HTML in a state having
          no interaction.
          """
          html_with_old_math_schema = (
              '<p>Value</p><oppia-noninteractive-math raw_latex-with-value="&a'
              'mp;quot;+,-,-,+&amp;quot;"></oppia-noninteractive-math>')
          html_with_new_math_schema = (
              '<p>Value</p><oppia-noninteractive-math math_content-with-value='
              '"{&amp;quot;raw_latex&amp;quot;: &amp;quot;+,-,-,+&amp;quot;, &'
              'amp;quot;svg_filename&amp;quot;: &amp;quot;&amp;quot;}"></oppia'
              '-noninteractive-math>')
  
          state_dict_with_old_math_schema = {
              'content': {
                  'content_id': 'content', 'html': html_with_old_math_schema
              },
              'param_changes': [],
              'content_ids_to_audio_translations': {'content': {}},
              'solicit_answer_details': False,
              'card_is_checkpoint': False,
              'linked_skill_id': None,
              'classifier_model_id': None,
              'interaction': {
                  'solution': None,
                  'answer_groups': [],
                  'default_outcome': {
                      'param_changes': [],
                      'feedback': {
                          'content_id': 'default_outcome',
                          'html': html_with_old_math_schema
                      },
                      'dest': 'Introduction',
                      'refresher_exploration_id': None,
                      'missing_prerequisite_skill_id': None,
                      'labelled_as_correct': False
                  },
                  'customization_args': None,
                  'confirmed_unclassified_answers': [],
                  'id': None,
                  'hints': [
                      {
                          'hint_content': {
                              'content_id': 'hint_1',
                              'html': html_with_old_math_schema
                          }
                      },
                      {
                          'hint_content': {
                              'content_id': 'hint_2',
                              'html': html_with_old_math_schema
                          }
                      }]
              }
          }
  
          state_dict_with_new_math_schema = {
              'content': {
                  'content_id': 'content', 'html': html_with_new_math_schema
              },
              'param_changes': [],
              'content_ids_to_audio_translations': {'content': {}},
              'solicit_answer_details': False,
              'card_is_checkpoint': False,
              'linked_skill_id': None,
              'classifier_model_id': None,
              'interaction': {
                  'solution': None,
                  'answer_groups': [],
                  'default_outcome': {
                      'param_changes': [],
                      'feedback': {
                          'content_id': 'default_outcome',
                          'html': html_with_new_math_schema
                      },
                      'dest': 'Introduction',
                      'refresher_exploration_id': None,
                      'missing_prerequisite_skill_id': None,
                      'labelled_as_correct': False
                  },
                  'customization_args': None,
                  'confirmed_unclassified_answers': [],
                  'id': None,
                  'hints': [
                      {
                          'hint_content': {
                              'content_id': 'hint_1',
                              'html': html_with_new_math_schema
                          }
                      },
                      {
                          'hint_content': {
                              'content_id': 'hint_2',
                              'html': html_with_new_math_schema
                          }
                      }]
              }
          }
          solution_dict = {
              'answer_is_exclusive': True,
              'correct_answer': 'Answer1',
              'explanation': {
                  'content_id': 'solution',
                  'html': html_with_old_math_schema
              }
          }
          self.assertEqual(
              state_domain.State.convert_html_fields_in_state(
                  state_dict_with_old_math_schema,
                  html_validation_service.
                  add_math_content_to_math_rte_components),
              state_dict_with_new_math_schema)
          # Assert that no action is performed on a solution dict when the
          # interaction ID is None.
          self.assertEqual(
              state_domain.Solution.convert_html_in_solution(
                  None, solution_dict,
                  html_validation_service.
                  add_math_content_to_math_rte_components,
                  rules_registry.Registry.get_html_field_types_to_rule_specs(),
                  {}
              ), solution_dict)
  
      def test_subtitled_html_validation_with_invalid_html_type(self):
          """Test validation of subtitled HTML with invalid html type."""
          subtitled_html = state_domain.SubtitledHtml(
              'content_id', '<p>some html</p>')
          subtitled_html.validate()
  
          with self.assertRaisesRegexp(
              utils.ValidationError, 'Invalid content HTML'
              ):
              with self.swap(subtitled_html, 'html', 20):
                  subtitled_html.validate()
  
      def test_subtitled_html_validation_with_invalid_content(self):
          """Test validation of subtitled HTML with invalid content."""
          subtitled_html = state_domain.SubtitledHtml(
              'content_id', '<p>some html</p>')
          subtitled_html.validate()
          with self.assertRaisesRegexp(
              utils.ValidationError, 'Expected content id to be a string, ' +
              'received 20'):
              with self.swap(subtitled_html, 'content_id', 20):
                  subtitled_html.validate()
  
      def test_subtitled_unicode_validation_with_invalid_html_type(self):
          """Test validation of subtitled unicode with invalid unicode type."""
          subtitled_unicode = state_domain.SubtitledUnicode(
              'content_id', 'some string')
          subtitled_unicode.validate()
  
          with self.assertRaisesRegexp(
              utils.ValidationError, 'Invalid content unicode'
              ):
              with self.swap(subtitled_unicode, 'unicode_str', 20):
                  subtitled_unicode.validate()
  
      def test_subtitled_unicode_validation_with_invalid_content(self):
          """Test validation of subtitled unicode with invalid content."""
          subtitled_unicode = state_domain.SubtitledUnicode(
              'content_id', 'some html string')
          subtitled_unicode.validate()
          with self.assertRaisesRegexp(
              utils.ValidationError, 'Expected content id to be a string, ' +
              'received 20'):
              with self.swap(subtitled_unicode, 'content_id', 20):
                  subtitled_unicode.validate()
  
      def test_voiceover_validation(self):
          """Test validation of voiceover."""
          audio_voiceover = state_domain.Voiceover('a.mp3', 20, True, 24.5)
          audio_voiceover.validate()
  
          with self.assertRaisesRegexp(
              utils.ValidationError, 'Expected audio filename to be a string'
              ):
              with self.swap(audio_voiceover, 'filename', 20):
                  audio_voiceover.validate()
          with self.assertRaisesRegexp(
              utils.ValidationError, 'Invalid audio filename'
              ):
              with self.swap(audio_voiceover, 'filename', '.invalidext'):
                  audio_voiceover.validate()
          with self.assertRaisesRegexp(
              utils.ValidationError, 'Invalid audio filename'
              ):
              with self.swap(audio_voiceover, 'filename', 'justanextension'):
                  audio_voiceover.validate()
          with self.assertRaisesRegexp(
              utils.ValidationError, 'Invalid audio filename'
              ):
              with self.swap(audio_voiceover, 'filename', 'a.invalidext'):
                  audio_voiceover.validate()
  
          with self.assertRaisesRegexp(
              utils.ValidationError, 'Expected file size to be an int'
              ):
              with self.swap(audio_voiceover, 'file_size_bytes', 'abc'):
                  audio_voiceover.validate()
          with self.assertRaisesRegexp(
              utils.ValidationError, 'Invalid file size'
              ):
              with self.swap(audio_voiceover, 'file_size_bytes', -3):
                  audio_voiceover.validate()
  
          with self.assertRaisesRegexp(
              utils.ValidationError, 'Expected needs_update to be a bool'
              ):
              with self.swap(audio_voiceover, 'needs_update', 'hello'):
                  audio_voiceover.validate()
          with self.assertRaisesRegexp(
              utils.ValidationError, 'Expected duration_secs to be a float'
              ):
              with self.swap(audio_voiceover, 'duration_secs', 'test'):
                  audio_voiceover.validate()
          with self.assertRaisesRegexp(
              utils.ValidationError, 'Expected duration_secs to be a float'
              ):
              with self.swap(audio_voiceover, 'duration_secs', 10):
                  audio_voiceover.validate()
          with self.assertRaisesRegexp(
              utils.ValidationError,
              'Expected duration_secs to be positive number, '
              'or zero if not yet specified'
              ):
              with self.swap(audio_voiceover, 'duration_secs', -3.45):
                  audio_voiceover.validate()
  
      def test_written_translation_validation(self):
          """Test validation of translation script."""
          written_translation = state_domain.WrittenTranslation(
              'html', 'Test.', True)
          written_translation.validate()
  
          with self.assertRaisesRegexp(
              AssertionError, 'Expected unicode HTML string, received 30'):
              with self.swap(written_translation, 'translation', 30):
                  written_translation.validate()
  
          with self.assertRaisesRegexp(
              utils.ValidationError, 'Expected needs_update to be a bool'
              ):
              with self.swap(written_translation, 'needs_update', 20):
                  written_translation.validate()
  
          with self.assertRaisesRegexp(
              utils.ValidationError, 'Invalid data_format'
              ):
              with self.swap(written_translation, 'data_format', 'int'):
                  written_translation.validate()
  
          with self.assertRaisesRegexp(
              utils.ValidationError, 'Invalid data_format'
              ):
              with self.swap(written_translation, 'data_format', 2):
                  written_translation.validate()
  
      def test_hints_validation(self):
          """Test validation of state hints."""
          exploration = exp_domain.Exploration.create_default_exploration('eid')
          exploration.objective = 'Objective'
          init_state = exploration.states[exploration.init_state_name]
          self.set_interaction_for_state(init_state, 'TextInput')
          exploration.validate()
  
          hints_list = [
              state_domain.Hint(
                  state_domain.SubtitledHtml('hint_1', '<p>hint one</p>')
              )
          ]
          init_state.update_interaction_hints(hints_list)
  
          solution_dict = {
              'answer_is_exclusive': False,
              'correct_answer': 'helloworld!',
              'explanation': {
                  'content_id': 'solution',
                  'html': '<p>hello_world is a string</p>'
              },
          }
  
          solution = state_domain.Solution.from_dict(
              init_state.interaction.id, solution_dict
          )
          init_state.update_interaction_solution(solution)
          exploration.validate()
  
          hints_list.append(
              state_domain.Hint(
                  state_domain.SubtitledHtml('hint_2', '<p>new hint</p>')
              )
          )
          init_state.update_interaction_hints(hints_list)
  
          self.assertEqual(
              init_state.interaction.hints[1].hint_content.html,
              '<p>new hint</p>')
  
          hints_list.append(
              state_domain.Hint(
                  state_domain.SubtitledHtml('hint_3', '<p>hint three</p>')
              )
          )
          init_state.update_interaction_hints(hints_list)
  
          del hints_list[1]
          init_state.update_interaction_hints(hints_list)
          init_state.update_next_content_id_index(4)
  
          self.assertEqual(len(init_state.interaction.hints), 2)
          exploration.validate()
  
      def test_update_customization_args_with_non_unique_content_ids(self):
          """Test that update customization args throws an error when passed
          customization args with non-unique content ids.
          """
          exploration = exp_domain.Exploration.create_default_exploration('eid')
          init_state = exploration.states[exploration.init_state_name]
          self.set_interaction_for_state(init_state, 'MultipleChoiceInput')
          with self.assertRaisesRegexp(
              Exception,
              'All customization argument content_ids should be unique.'
          ):
              init_state.update_interaction_customization_args({
                  'choices': {
                      'value': [{
                          'content_id': 'non-unique-content-id',
                          'html': '1'
                      }, {
                          'content_id': 'non-unique-content-id',
                          'html': '2'
                      }]
                  },
                  'showChoicesInShuffledOrder': {'value': True}
              })
  
      def test_solution_validation(self):
          """Test validation of state solution."""
          exploration = exp_domain.Exploration.create_default_exploration('eid')
          exploration.objective = 'Objective'
          init_state = exploration.states[exploration.init_state_name]
          self.set_interaction_for_state(init_state, 'TextInput')
          exploration.validate()
  
          # Solution should be set to None as default.
          self.assertEqual(init_state.interaction.solution, None)
  
          hints_list = [
              state_domain.Hint(
                  state_domain.SubtitledHtml('hint_1', '')
              )
          ]
          init_state.update_interaction_hints(hints_list)
          solution_dict = {
              'answer_is_exclusive': False,
              'correct_answer': [0, 0],
              'explanation': {
                  'content_id': 'solution',
                  'html': '<p>hello_world is a string</p>'
              }
          }
  
          # Object type of answer must match that of correct_answer.
          with self.assertRaisesRegexp(
              AssertionError,
              re.escape('Expected unicode string, received [0, 0]')
          ):
              init_state.interaction.solution = (
                  state_domain.Solution.from_dict(
                      init_state.interaction.id, solution_dict))
  
          solution_dict = {
              'answer_is_exclusive': False,
              'correct_answer': 'hello_world!',
              'explanation': {
                  'content_id': 'solution',
                  'html': '<p>hello_world is a string</p>'
              }
          }
          init_state.update_interaction_solution(
              state_domain.Solution.from_dict(
                  init_state.interaction.id, solution_dict))
          exploration.validate()
  
      def test_validate_state_unique_content_ids(self):
          exploration = exp_domain.Exploration.create_default_exploration('eid')
          init_state = exploration.states[exploration.init_state_name]
          init_state.update_interaction_id('MultipleChoiceInput')
          init_state.update_interaction_customization_args({
              'choices': {
                  'value': [{
                      'content_id': '',
                      'html': 'one'
                  }]
              },
              'showChoicesInShuffledOrder': {'value': True}
          })
  
          with self.assertRaisesRegexp(
              utils.ValidationError,
              'Expected all content_ids to be unique, received'
          ):
              with self.swap(
                  init_state.interaction.customization_args['choices'].value[0],
                  'content_id',
                  'content'
              ):
                  exploration.validate()
  
      def test_validate_state_content_id_indexes(self):
          exploration = exp_domain.Exploration.create_default_exploration('eid')
          init_state = exploration.states[exploration.init_state_name]
          init_state.update_interaction_id('MultipleChoiceInput')
          init_state.update_interaction_customization_args({
              'choices': {
                  'value': [{
                      'content_id': 'ca_choices_10',
                      'html': 'one'
                  }]
              },
              'showChoicesInShuffledOrder': {'value': True}
          })
          init_state.update_next_content_id_index(9)
  
          with self.assertRaisesRegexp(
              utils.ValidationError,
              'Expected all content id indexes to be less than the "next '
              'content id index"'
          ):
              exploration.validate()
  
      def test_validate_state_solicit_answer_details(self):
          """Test validation of solicit_answer_details."""
          exploration = exp_domain.Exploration.create_default_exploration('eid')
          init_state = exploration.states[exploration.init_state_name]
          self.assertEqual(init_state.solicit_answer_details, False)
          with self.assertRaisesRegexp(
              utils.ValidationError, 'Expected solicit_answer_details to be ' +
              'a boolean, received'):
              with self.swap(init_state, 'solicit_answer_details', 'abc'):
                  exploration.validate()
          self.assertEqual(init_state.solicit_answer_details, False)
          self.set_interaction_for_state(init_state, 'Continue')
          self.assertEqual(init_state.interaction.id, 'Continue')
          exploration.validate()
          with self.assertRaisesRegexp(
              utils.ValidationError, 'The Continue interaction does not ' +
              'support soliciting answer details from learners.'):
              with self.swap(init_state, 'solicit_answer_details', True):
                  exploration.validate()
          self.set_interaction_for_state(init_state, 'TextInput')
          self.assertEqual(init_state.interaction.id, 'TextInput')
          self.assertEqual(init_state.solicit_answer_details, False)
          exploration.validate()
          init_state.solicit_answer_details = True
          self.assertEqual(init_state.solicit_answer_details, True)
          exploration.validate()
          init_state = exploration.states[exploration.init_state_name]
          self.assertEqual(init_state.solicit_answer_details, True)
  
      def test_validate_state_linked_skill_id(self):
          """Test validation of linked_skill_id."""
          exploration = exp_domain.Exploration.create_default_exploration('eid')
          init_state = exploration.states[exploration.init_state_name]
          self.assertEqual(init_state.linked_skill_id, None)
          with self.assertRaisesRegexp(
              utils.ValidationError, 'Expected linked_skill_id to be ' +
              'a str, received 12.'):
              with self.swap(init_state, 'linked_skill_id', 12):
                  exploration.validate()
          self.assertEqual(init_state.linked_skill_id, None)
  
      def test_validate_state_card_is_checkpoint(self):
          """Test validation of card_is_checkpoint."""
          exploration = exp_domain.Exploration.create_default_exploration('eid')
          init_state = exploration.states[exploration.init_state_name]
          self.assertEqual(init_state.card_is_checkpoint, True)
          with self.assertRaisesRegexp(
              utils.ValidationError, 'Expected card_is_checkpoint to be ' +
              'a boolean, received'):
              with self.swap(init_state, 'card_is_checkpoint', 'abc'):
                  exploration.validate()
          self.assertEqual(init_state.card_is_checkpoint, True)
  
      def test_validate_solution_answer_is_exclusive(self):
          exploration = self.save_new_valid_exploration('exp_id', 'owner_id')
  
          # Solution should be set to None as default.
          self.assertEqual(exploration.init_state.interaction.solution, None)
  
          solution_dict = {
              'answer_is_exclusive': False,
              'correct_answer': 'hello_world!',
              'explanation': {
                  'content_id': 'solution',
                  'html': '<p>hello_world is a string</p>'
              }
          }
          hints_list = [
              state_domain.Hint(
                  state_domain.SubtitledHtml('hint_1', '')
              )
          ]
          solution = state_domain.Solution.from_dict(
              exploration.init_state.interaction.id, solution_dict)
          exploration.init_state.update_interaction_hints(hints_list)
          exploration.init_state.update_interaction_solution(solution)
          exploration.validate()
  
          solution_dict = {
              'answer_is_exclusive': 1,
              'correct_answer': 'hello_world!',
              'explanation': {
                  'content_id': 'solution',
                  'html': '<p>hello_world is a string</p>'
              }
          }
          solution = state_domain.Solution.from_dict(
              exploration.init_state.interaction.id, solution_dict)
          exploration.init_state.update_interaction_solution(solution)
          with self.assertRaisesRegexp(
              Exception, 'Expected answer_is_exclusive to be bool, received 1'):
              exploration.validate()
  
      def test_validate_non_list_param_changes(self):
          exploration = self.save_new_valid_exploration('exp_id', 'owner_id')
          exploration.init_state.param_changes = 0
  
          with self.assertRaisesRegexp(
              Exception, 'Expected state param_changes to be a list, received 0'):
              exploration.init_state.validate(None, True)
  
      def test_validate_duplicate_content_id_with_answer_group_feedback(self):
          exploration = self.save_new_valid_exploration('exp_id', 'owner_id')
          state_answer_group = state_domain.AnswerGroup(
              state_domain.Outcome(
                  exploration.init_state_name, state_domain.SubtitledHtml(
                      'feedback_1', '<p>Feedback</p>'),
                  False, [], None, None),
              [
                  state_domain.RuleSpec(
                      'Contains',
                      {
                          'x': {
                              'contentId': 'rule_input_Contains',
                              'normalizedStrSet': ['Test']
                              }
                      })
              ],
              [],
              None
          )
  
          exploration.init_state.update_interaction_answer_groups(
              [state_answer_group])
          exploration.init_state.update_content(
              state_domain.SubtitledHtml.from_dict({
                  'content_id': 'feedback_1',
                  'html': '<p>Feedback</p>'
              }))
  
          with self.assertRaisesRegexp(
              Exception, 'Found a duplicate content id feedback_1'):
              exploration.init_state.validate(None, True)
  
      def test_validate_duplicate_content_id_with_answer_group_rules(self):
          exploration = self.save_new_valid_exploration('exp_id', 'owner_id')
          state_answer_group = state_domain.AnswerGroup(
              state_domain.Outcome(
                  exploration.init_state_name, state_domain.SubtitledHtml(
                      'feedback_1', '<p>Feedback</p>'),
                  False, [], None, None),
              [
                  state_domain.RuleSpec(
                      'Contains',
                      {
                          'x': {
                              'contentId': 'rule_input_Contains',
                              'normalizedStrSet': ['Test']
                              }
                      }),
                  state_domain.RuleSpec(
                      'Contains',
                      {
                          'x': {
                              'contentId': 'rule_input_Contains',
                              'normalizedStrSet': ['Test1']
                              }
                      })
              ],
              [],
              None
          )
  
          exploration.init_state.update_interaction_answer_groups(
              [state_answer_group])
  
          with self.assertRaisesRegexp(
              Exception, 'Found a duplicate content id rule_input_Contains'):
              exploration.init_state.validate(None, True)
  
      def test_validate_duplicate_content_id_with_default_outcome(self):
          exploration = self.save_new_valid_exploration('exp_id', 'owner_id')
          default_outcome = state_domain.Outcome(
              'Introduction', state_domain.SubtitledHtml('default_outcome', ''),
              False, [], None, None
          )
          exploration.init_state.update_interaction_default_outcome(
              default_outcome
          )
          exploration.init_state.update_content(
              state_domain.SubtitledHtml.from_dict({
                  'content_id': 'default_outcome',
                  'html': ''
              }))
  
          with self.assertRaisesRegexp(
              Exception, 'Found a duplicate content id default_outcome'):
              exploration.init_state.validate(None, True)
  
      def test_validate_duplicate_content_id_with_hints(self):
          exploration = self.save_new_valid_exploration('exp_id', 'owner_id')
          hints_list = [
              state_domain.Hint(
                  state_domain.SubtitledHtml('hint_1', '<p>some html</p>')
              )
          ]
  
          exploration.init_state.update_interaction_hints(hints_list)
          exploration.init_state.update_content(
              state_domain.SubtitledHtml.from_dict({
                  'content_id': 'hint_1',
                  'html': ''
              }))
  
          with self.assertRaisesRegexp(
              Exception, 'Found a duplicate content id hint_1'):
              exploration.init_state.validate(None, True)
  
      def test_validate_duplicate_content_id_with_solution(self):
          exploration = self.save_new_valid_exploration('exp_id', 'owner_id')
          subtitled_html = state_domain.SubtitledHtml('content_id', 'some html')
  
          hints_list = [state_domain.Hint(subtitled_html)]
  
          exploration.init_state.interaction.hints = hints_list
          solution_dict = {
              'answer_is_exclusive': True,
              'correct_answer': 'hello_world!',
              'explanation': {
                  'content_id': 'solution',
                  'html': '<p>hello_world is a string</p>'
              }
          }
          solution = state_domain.Solution.from_dict(
              exploration.init_state.interaction.id, solution_dict)
          exploration.init_state.update_interaction_solution(solution)
          exploration.init_state.update_content(
              state_domain.SubtitledHtml.from_dict({
                  'content_id': 'solution',
                  'html': ''
                  }))
  
          with self.assertRaisesRegexp(
              Exception, 'Found a duplicate content id solution'):
              exploration.init_state.validate(None, True)
  
      def test_cannot_convert_state_dict_to_yaml_with_invalid_state_dict(self):
          exploration = self.save_new_valid_exploration('exp_id', 'owner_id')
  
          with python_utils.ExitStack() as stack:
              captured_logs = stack.enter_context(
                  self.capture_logging(min_level=logging.ERROR))
              stack.enter_context(
                  self.assertRaisesRegexp(
                      Exception, 'string indices must be integers')
              )
  
              exploration.init_state.convert_state_dict_to_yaml(
                  'invalid_state_dict', 10)
  
          self.assertEqual(len(captured_logs), 1)
          self.assertIn('Bad state dict: invalid_state_dict', captured_logs[0])
  
      def test_cannot_update_hints_with_content_id_not_in_written_translations(
              self):
          exploration = self.save_new_valid_exploration('exp_id', 'owner_id')
          old_hints_list = [
              state_domain.Hint(
                  state_domain.SubtitledHtml(
                      'hint_1', '<p>Hello, this is html1 for state2</p>')
              )
          ]
          new_hints_list = [
              state_domain.Hint(
                  state_domain.SubtitledHtml(
                      'hint_2', '<p>Hello, this is html2 for state2</p>')
              )
          ]
  
          exploration.init_state.update_interaction_hints(old_hints_list)
  
          written_translations_dict = {
              'translations_mapping': {
                  'content': {
                      'hi': {
                          'data_format': 'html',
                          'translation': '<p>Test!</p>',
                          'needs_update': True
                      }
                  },
                  'default_outcome': {}
              }
          }
          written_translations = (
              state_domain.WrittenTranslations.from_dict(
                  written_translations_dict))
  
          exploration.init_state.update_written_translations(written_translations)
  
          with self.assertRaisesRegexp(
              Exception,
              'The content_id hint_1 does not exist in written_translations'):
              exploration.init_state.update_interaction_hints(new_hints_list)
  
      def test_cannot_update_hints_with_content_id_not_in_recorded_voiceovers(
              self):
          exploration = self.save_new_valid_exploration('exp_id', 'owner_id')
          old_hints_list = [
              state_domain.Hint(
                  state_domain.SubtitledHtml(
                      'hint_1', '<p>Hello, this is html1 for state2</p>')
              )
          ]
          new_hints_list = [
              state_domain.Hint(
                  state_domain.SubtitledHtml(
                      'hint_2', '<p>Hello, this is html2 for state2</p>')
              )
          ]
  
          exploration.init_state.update_interaction_hints(old_hints_list)
  
          recorded_voiceovers_dict = {
              'voiceovers_mapping': {
                  'content': {
                      'en': {
                          'filename': 'filename3.mp3',
                          'file_size_bytes': 3000,
                          'needs_update': False,
                          'duration_secs': 8.1
                      }
                  },
                  'default_outcome': {}
              }
          }
          recorded_voiceovers = (
              state_domain.RecordedVoiceovers.from_dict(recorded_voiceovers_dict))
  
          exploration.init_state.update_recorded_voiceovers(recorded_voiceovers)
  
          with self.assertRaisesRegexp(
              Exception,
              'The content_id hint_1 does not exist in recorded_voiceovers'):
              exploration.init_state.update_interaction_hints(new_hints_list)
  
      def test_cannot_update_hints_with_new_content_id_in_written_translations(
              self):
          exploration = self.save_new_valid_exploration('exp_id', 'owner_id')
          old_hints_list = [
              state_domain.Hint(
                  state_domain.SubtitledHtml(
                      'hint_1', '<p>Hello, this is html1 for state2</p>')
              )
          ]
          new_hints_list = [
              state_domain.Hint(
                  state_domain.SubtitledHtml(
                      'hint_2', '<p>Hello, this is html2 for state2</p>')
              )
          ]
  
          exploration.init_state.update_interaction_hints(old_hints_list)
  
          written_translations_dict = {
              'translations_mapping': {
                  'hint_2': {
                      'hi': {
                          'data_format': 'html',
                          'translation': '<p>Test!</p>',
                          'needs_update': True
                      }
                  },
                  'hint_1': {
                      'hi': {
                          'data_format': 'html',
                          'translation': '<p>Test1!</p>',
                          'needs_update': True
                      }
                  },
                  'default_outcome': {}
              }
          }
          written_translations = (
              state_domain.WrittenTranslations.from_dict(
                  written_translations_dict))
  
          exploration.init_state.update_written_translations(written_translations)
  
          with self.assertRaisesRegexp(
              Exception,
              'The content_id hint_2 already exists in written_translations'):
              exploration.init_state.update_interaction_hints(new_hints_list)
  
      def test_cannot_update_hints_with_new_content_id_in_recorded_voiceovers(
              self):
          exploration = self.save_new_valid_exploration('exp_id', 'owner_id')
          old_hints_list = [
              state_domain.Hint(
                  state_domain.SubtitledHtml(
                      'hint_1', '<p>Hello, this is html1 for state2</p>')
              )
          ]
          new_hints_list = [
              state_domain.Hint(
                  state_domain.SubtitledHtml(
                      'hint_2', '<p>Hello, this is html2 for state2</p>')
              )
          ]
  
          exploration.init_state.update_interaction_hints(old_hints_list)
  
          recorded_voiceovers_dict = {
              'voiceovers_mapping': {
                  'hint_1': {
                      'en': {
                          'filename': 'filename3.mp3',
                          'file_size_bytes': 3000,
                          'needs_update': False,
                          'duration_secs': 6.1
                      }
                  },
                  'hint_2': {
                      'en': {
                          'filename': 'filename4.mp3',
                          'file_size_bytes': 3000,
                          'needs_update': False,
                          'duration_secs': 7.5
                      }
                  },
                  'default_outcome': {}
              }
          }
          recorded_voiceovers = (
              state_domain.RecordedVoiceovers.from_dict(recorded_voiceovers_dict))
  
          exploration.init_state.update_recorded_voiceovers(recorded_voiceovers)
  
          with self.assertRaisesRegexp(
              Exception,
              'The content_id hint_2 already exists in recorded_voiceovers'):
              exploration.init_state.update_interaction_hints(new_hints_list)
  
      def test_cannot_update_interaction_solution_with_non_dict_solution(self):
          exploration = self.save_new_valid_exploration('exp_id', 'owner_id')
          hints_list = [
              state_domain.Hint(
                  state_domain.SubtitledHtml(
                      'hint_1', '<p>Hello, this is html1 for state2</p>')
              )
          ]
          solution_dict = {
              'answer_is_exclusive': True,
              'correct_answer': u'hello_world!',
              'explanation': {
                  'content_id': 'solution',
                  'html': u'<p>hello_world is a string</p>'
              }
          }
          solution = state_domain.Solution.from_dict(
              exploration.init_state.interaction.id, solution_dict)
          exploration.init_state.update_interaction_hints(hints_list)
          exploration.init_state.update_interaction_solution(solution)
  
          self.assertEqual(
              exploration.init_state.interaction.solution.to_dict(),
              solution_dict)
  
          with self.assertRaisesRegexp(
              Exception, 'Expected solution to be a Solution object,'
              'received test string'):
              exploration.init_state.update_interaction_solution('test string')
  
      def test_update_interaction_solution_with_no_solution(self):
          exploration = self.save_new_valid_exploration('exp_id', 'owner_id')
          hints_list = [
              state_domain.Hint(
                  state_domain.SubtitledHtml(
                      'hint_1', '<p>Hello, this is html1 for state2</p>'
                  )
              )
          ]
  
          exploration.init_state.update_interaction_hints(hints_list)
          exploration.init_state.update_interaction_solution(None)
  
          self.assertIsNone(exploration.init_state.interaction.solution)
  
      def test_cannot_update_interaction_hints_with_non_list_hints(self):
          exploration = self.save_new_valid_exploration('exp_id', 'owner_id')
  
          with self.assertRaisesRegexp(
              Exception, 'Expected hints_list to be a list'):
              exploration.init_state.update_interaction_hints({})
  
      def test_cannot_update_non_list_interaction_confirmed_unclassified_answers(
              self):
          exploration = self.save_new_valid_exploration('exp_id', 'owner_id')
  
          with self.assertRaisesRegexp(
              Exception, 'Expected confirmed_unclassified_answers to be a list'):
              (
                  exploration.init_state
                  .update_interaction_confirmed_unclassified_answers({}))
  
      def test_update_interaction_confirmed_unclassified_answers(self):
          exploration = self.save_new_valid_exploration('exp_id', 'owner_id')
          state_answer_group = state_domain.AnswerGroup(
              state_domain.Outcome(
                  exploration.init_state_name, state_domain.SubtitledHtml(
                      'feedback_1', '<p>Feedback</p>'),
                  False, [], None, None),
              [
                  state_domain.RuleSpec(
                      'Contains',
                      {
                          'x': 'Test'
                      })
              ],
              [],
              None
          )
  
          self.assertEqual(
              exploration.init_state.interaction.confirmed_unclassified_answers,
              [])
  
          (
              exploration.init_state
              .update_interaction_confirmed_unclassified_answers(
                  [state_answer_group])
          )
  
          self.assertEqual(
              exploration.init_state.interaction.confirmed_unclassified_answers,
              [state_answer_group])
  
      def test_cannot_update_non_list_interaction_answer_groups(self):
          exploration = self.save_new_valid_exploration('exp_id', 'owner_id')
  
          with self.assertRaisesRegexp(
              Exception, 'Expected interaction_answer_groups to be a list'):
              exploration.init_state.update_interaction_answer_groups(
                  'invalid_answer_groups')
  
      def test_cannot_update_answer_groups_with_non_dict_rule_inputs(self):
          exploration = self.save_new_valid_exploration('exp_id', 'owner_id')
          state_answer_group = state_domain.AnswerGroup(
              state_domain.Outcome(
                  exploration.init_state_name, state_domain.SubtitledHtml(
                      'feedback_1', '<p>Feedback</p>'),
                  False, [], None, None),
              [
                  state_domain.RuleSpec(
                      'Contains', []
                      )
              ],
              [],
              None
          )
  
          with self.assertRaisesRegexp(
              Exception,
              re.escape('Expected rule_inputs to be a dict, received []')
          ):
              exploration.init_state.update_interaction_answer_groups(
                  [state_answer_group])
  
      def test_cannot_update_answer_groups_with_non_list_rule_specs(self):
          exploration = self.save_new_valid_exploration('exp_id', 'owner_id')
          state_answer_group = state_domain.AnswerGroup(
              state_domain.Outcome(
                  exploration.init_state_name, state_domain.SubtitledHtml(
                      'feedback_1', '<p>Feedback</p>'), False, [], None, None
              ), {}, [], None
          )
          state_answer_group.rule_specs = {}
  
          with self.assertRaisesRegexp(
              Exception, 'Expected answer group rule specs to be a list'):
              exploration.init_state.update_interaction_answer_groups(
                  [state_answer_group])
  
      def test_cannot_update_answer_groups_with_invalid_rule_input_value(self):
          exploration = self.save_new_valid_exploration('exp_id', 'owner_id')
          state_answer_group = state_domain.AnswerGroup(
              state_domain.Outcome(
                  exploration.init_state_name, state_domain.SubtitledHtml(
                      'feedback_1', '<p>Feedback</p>'),
                  False, [], None, None),
              [
                  state_domain.RuleSpec(
                      'Contains',
                      {
                          'x': {
                              'contentId': 'rule_input_Equals',
                              'normalizedStrSet': [[]]
                              }
                      })
              ],
              [],
              None
          )
  
          with self.assertRaisesRegexp(
              Exception,
              re.escape(
                  'Value has the wrong type. It should be a TranslatableSetOf'
                  'NormalizedString. The value is'
              )
          ):
              exploration.init_state.update_interaction_answer_groups(
                  [state_answer_group])
  
      def test_validate_rule_spec(self):
          observed_log_messages = []
  
          def _mock_logging_function(msg, *args):
              """Mocks logging.error()."""
              observed_log_messages.append(msg % args)
  
          logging_swap = self.swap(logging, 'warning', _mock_logging_function)
  
          exploration = self.save_new_valid_exploration('exp_id', 'owner_id')
          state_answer_group = state_domain.AnswerGroup(
              state_domain.Outcome(
                  exploration.init_state_name, state_domain.SubtitledHtml(
                      'feedback_1', '<p>Feedback</p>'),
                  False, [], None, None),
              [
                  state_domain.RuleSpec(
                      'Contains',
                      {
                          'x': {
                              'contentId': 'rule_input_Equals',
                              'normalizedStrSet': ['Test']
                              }
                      })
              ],
              [],
              None
          )
          exploration.init_state.update_interaction_answer_groups(
              [state_answer_group])
  
          with logging_swap, self.assertRaisesRegexp(KeyError, '\'x\''):
              (
                  exploration.init_state.interaction.answer_groups[0]
                  .rule_specs[0].validate([], {})
              )
  
          self.assertEqual(
              observed_log_messages,
              [
                  'RuleSpec \'Contains\' has inputs which are not recognized '
                  'parameter names: {\'x\'}'
              ]
          )
  
  
  class InteractionCustomizationArgDomainTests(test_utils.GenericTestBase):
      """Test methods for InteractionCustomizationArg domain object."""
  
      def test_traverse_by_schema_and_convert(self):
          html = []
          def extract_html(value, unused_schema_obj_type):
              """Extracts html from SubtitledHtml values.
  
              Args:
                  value: SubtitledHtml|SubtitledUnicode. The value in the
                      customization argument value to be converted.
                  unused_schema_obj_type: str. The schema obj_type for the
                      customization argument value, which is one of
                      'SubtitledUnicode' or 'SubtitledHtml'.
  
              Returns:
                  SubtitledHtml|SubtitledUnicode. The converted SubtitledHtml
                  object, if schema_type is 'SubititledHtml', otherwise the
                  unmodified SubtitledUnicode object.
              """
              html.append(value.html)
              return html
  
          schema = {
              'type': 'dict',
              'properties': [{
                  'name': 'content',
                  'schema': {
                      'type': 'custom',
                      'obj_type': 'SubtitledHtml',
                  }
              }]
          }
          value = {
              'content': state_domain.SubtitledHtml('id', '<p>testing</p>')
          }
  
          state_domain.InteractionCustomizationArg.traverse_by_schema_and_convert(
              schema, value, extract_html)
  
          self.assertEqual(html, ['<p>testing</p>'])
  
      def test_traverse_by_schema_and_get(self):
          html = []
  
          schema = {
              'type': 'dict',
              'properties': [{
                  'name': 'content',
                  'schema': {
                      'type': 'custom',
                      'obj_type': 'SubtitledHtml',
                  }
              }]
          }
          value = {
              'content': state_domain.SubtitledHtml('id', '<p>testing</p>')
          }
  
          html = (
              state_domain.InteractionCustomizationArg.traverse_by_schema_and_get(
                  schema,
                  value,
                  [schema_utils.SCHEMA_OBJ_TYPE_SUBTITLED_HTML],
                  lambda x: x.html)
          )
  
          self.assertEqual(html, ['<p>testing</p>'])
  
  
  class SubtitledUnicodeDomainUnitTests(test_utils.GenericTestBase):
      """Test SubtitledUnicode domain object methods."""
  
      def test_from_and_to_dict(self):
          subtitled_unicode_dict = {
              'content_id': 'id',
              'unicode_str': ''
          }
          subtitled_unicode = state_domain.SubtitledUnicode.from_dict(
              subtitled_unicode_dict)
          self.assertEqual(subtitled_unicode.to_dict(), subtitled_unicode_dict)
  
      def test_create_default(self):
          subtitled_unicode = (
              state_domain.SubtitledUnicode.create_default_subtitled_unicode(
                  'id')
          )
          self.assertEqual(subtitled_unicode.to_dict(), {
              'content_id': 'id',
              'unicode_str': ''
          })
  
  
  class WrittenTranslationsDomainUnitTests(test_utils.GenericTestBase):
      """Test methods operating on written transcripts."""
  
      def test_data_formats_are_correct_and_complete(self):
          translatable_class_names_in_data_formats = sorted(
              state_domain.WrittenTranslation.
              DATA_FORMAT_TO_TRANSLATABLE_OBJ_TYPE.values())
          self.assertEqual(
              translatable_class_names_in_data_formats,
              translatable_object_registry.Registry.get_all_class_names())
  
      def test_from_and_to_dict_works_correctly(self):
          written_translations_dict = {
              'translations_mapping': {
                  'content1': {
                      'en': {
                          'data_format': 'html',
                          'translation': 'hello',
                          'needs_update': True
                      },
                      'hi': {
                          'data_format': 'html',
                          'translation': 'Hey!',
                          'needs_update': False
                      },
                      'fr': {
                          'data_format': 'set_of_normalized_string',
                          'translation': ['test1', 'test2'],
                          'needs_update': False
                      },
                  },
                  'feedback_1': {
                      'hi': {
                          'data_format': 'html',
                          'translation': 'Testing!',
                          'needs_update': False
                      },
                      'en': {
                          'data_format': 'html',
                          'translation': 'hello!',
                          'needs_update': False
                      },
                      'fr': {
                          'data_format': 'set_of_normalized_string',
                          'translation': ['test1', 'test2'],
                          'needs_update': False
                      }
                  }
              }
          }
  
          written_translations = state_domain.WrittenTranslations.from_dict(
              written_translations_dict)
          self.assertEqual(
              written_translations.to_dict(), written_translations_dict)
  
      def test_get_content_ids_for_text_translation_return_correct_list_of_content_id(self): # pylint: disable=line-too-long
          written_translations = state_domain.WrittenTranslations.from_dict({
              'translations_mapping': {}
          })
          self.assertEqual(
              written_translations.get_content_ids_for_text_translation(), [])
  
          written_translations.add_content_id_for_translation('feedback_1')
          written_translations.add_content_id_for_translation('feedback_2')
          self.assertItemsEqual(
              written_translations.get_content_ids_for_text_translation(), [
                  'feedback_2', 'feedback_1'])
  
      def test_get_translated_content_in_non_existing_language_raise_error(self):
          written_translations = state_domain.WrittenTranslations.from_dict({
              'translations_mapping': {
                  'content': {
                      'en': {
                          'data_format': 'html',
                          'translation': '<p> In English.</p>',
                          'needs_update': False
                      }
                  }
              }
          })
          translated_content = written_translations.get_translated_content(
              'content', 'en')
          self.assertEqual(translated_content, '<p> In English.</p>')
  
          with self.assertRaisesRegexp(
              Exception, 'Translation for the given content_id content does not '
              'exist in hi language code'):
              written_translations.get_translated_content('content', 'hi')
  
      def test_get_translated_content_for_invalid_content_id_raise_error(self):
          written_translations = state_domain.WrittenTranslations.from_dict({
              'translations_mapping': {
                  'content': {
                      'en': {
                          'data_format': 'html',
                          'translation': '<p> In English.</p>',
                          'needs_update': False
                      }
                  }
              }
          })
          translated_content = written_translations.get_translated_content(
              'content', 'en')
          self.assertEqual(translated_content, '<p> In English.</p>')
  
          with self.assertRaisesRegexp(
              Exception, 'Invalid content_id: invalid_id'):
              written_translations.get_translated_content('invalid_id', 'hi')
  
      def test_add_content_id_for_translations_adds_content_id(self):
          written_translations = state_domain.WrittenTranslations.from_dict({
              'translations_mapping': {}
          })
  
          self.assertEqual(
              len(written_translations.get_content_ids_for_text_translation()), 0)
  
          new_content_id = 'content_id'
          written_translations.add_content_id_for_translation(new_content_id)
  
          self.assertEqual(
              len(written_translations.get_content_ids_for_text_translation()), 1)
          self.assertEqual(
              written_translations.get_content_ids_for_text_translation(),
              ['content_id'])
  
      def test_add_content_id_for_translation_with_invalid_content_id_raise_error(
              self):
          written_translations = state_domain.WrittenTranslations.from_dict({
              'translations_mapping': {}
          })
          invalid_content_id = 123
          with self.assertRaisesRegexp(
              Exception, 'Expected content_id to be a string, received 123'):
              written_translations.add_content_id_for_translation(
                  invalid_content_id)
  
      def test_add_content_id_for_translation_with_existing_content_id_raise_error( # pylint: disable=line-too-long
              self):
          written_translations_dict = {
              'translations_mapping': {
                  'feedback_1': {
                      'en': {
                          'data_format': 'html',
                          'translation': 'hello!',
                          'needs_update': False
                      }
                  }
              }
          }
  
          written_translations = state_domain.WrittenTranslations.from_dict(
              written_translations_dict)
          existing_content_id = 'feedback_1'
          with self.assertRaisesRegexp(
              Exception, 'The content_id feedback_1 already exist.'):
              written_translations.add_content_id_for_translation(
                  existing_content_id)
  
      def test_delete_content_id_for_translations_deletes_content_id(self):
          old_written_translations_dict = {
              'translations_mapping': {
                  'content': {
                      'en': {
                          'data_format': 'html',
                          'translation': 'hello!',
                          'needs_update': False
                      }
                  }
              }
          }
  
          written_translations = state_domain.WrittenTranslations.from_dict(
              old_written_translations_dict)
          self.assertEqual(
              len(written_translations.get_content_ids_for_text_translation()), 1)
  
          written_translations.delete_content_id_for_translation('content')
  
          self.assertEqual(
              len(written_translations.get_content_ids_for_text_translation()), 0)
  
      def test_delete_content_id_for_translation_with_nonexisting_content_id_raise_error(self): # pylint: disable=line-too-long
          written_translations_dict = {
              'translations_mapping': {
                  'content': {}
              }
          }
          written_translations = state_domain.WrittenTranslations.from_dict(
              written_translations_dict)
          nonexisting_content_id_to_delete = 'feedback_1'
          with self.assertRaisesRegexp(
              Exception, 'The content_id feedback_1 does not exist.'):
              written_translations.delete_content_id_for_translation(
                  nonexisting_content_id_to_delete)
  
      def test_delete_content_id_for_translation_with_invalid_content_id_raise_error(self): # pylint: disable=line-too-long
          written_translations = state_domain.WrittenTranslations.from_dict({
              'translations_mapping': {}
          })
          invalid_content_id_to_delete = 123
          with self.assertRaisesRegexp(
              Exception, 'Expected content_id to be a string, '):
              written_translations.delete_content_id_for_translation(
                  invalid_content_id_to_delete)
  
      def test_validation_with_invalid_content_id_raise_error(self):
          written_translations_dict = {
              'translations_mapping': {
                  123: {}
              }
          }
  
          written_translations = state_domain.WrittenTranslations.from_dict(
              written_translations_dict)
  
          with self.assertRaisesRegexp(
              Exception, 'Expected content_id to be a string, '):
              written_translations.validate([123])
  
      def test_validate_non_dict_language_code_to_written_translation(self):
          written_translations = state_domain.WrittenTranslations({
              'en': []
          })
  
          with self.assertRaisesRegexp(
              Exception,
              re.escape('Expected content_id value to be a dict, received []')):
              written_translations.validate(None)
  
      def test_validation_with_invalid_type_language_code_raise_error(self):
          written_translations_dict = {
              'translations_mapping': {
                  'content': {
                      123: {
                          'data_format': 'html',
                          'translation': 'hello!',
                          'needs_update': False
                      }
                  }
              }
          }
  
          written_translations = state_domain.WrittenTranslations.from_dict(
              written_translations_dict)
  
          with self.assertRaisesRegexp(
              Exception, 'Expected language_code to be a string, '):
              written_translations.validate(['content'])
  
      def test_validation_with_unknown_language_code_raise_error(self):
          written_translations_dict = {
              'translations_mapping': {
                  'content': {
                      'ed': {
                          'data_format': 'html',
                          'translation': 'hello!',
                          'needs_update': False
                      }
                  }
              }
          }
  
          written_translations = state_domain.WrittenTranslations.from_dict(
              written_translations_dict)
  
          with self.assertRaisesRegexp(Exception, 'Invalid language_code: ed'):
              written_translations.validate(['content'])
  
      def test_validation_with_invalid_content_id_list(self):
          written_translations_dict = {
              'translations_mapping': {
                  'content': {
                      'en': {
                          'data_format': 'html',
                          'translation': '<p>hello!</p>',
                          'needs_update': False
                      }
                  }
              }
          }
  
          written_translations = state_domain.WrittenTranslations.from_dict(
              written_translations_dict)
  
          with self.assertRaisesRegexp(
              Exception,
              re.escape(
                  'Expected state written_translations to match the listed '
                  'content ids [\'invalid_content\']')):
              written_translations.validate(['invalid_content'])
  
      def test_get_content_ids_that_are_correctly_translated(self):
          written_translations_dict = {
              'translations_mapping': {
                  'content': {},
                  'hint_1': {}
              }
          }
  
          written_translations = state_domain.WrittenTranslations.from_dict(
              written_translations_dict)
  
          self.assertEqual(
              written_translations.get_content_ids_that_are_correctly_translated(
                  'hi'), [])
  
      def test_get_content_ids_that_are_correctly_translated_with_some_existing_translations(self): # pylint: disable=line-too-long
          written_translations_dict = {
              'translations_mapping': {
                  'content': {
                      'hi': {
                          'data_format': 'html',
                          'translation': '<p>hello!</p>',
                          'needs_update': False
                      }
                  },
                  'hint_1': {}
              }
          }
          written_translations = state_domain.WrittenTranslations.from_dict(
              written_translations_dict)
  
          self.assertEqual(
              written_translations.get_content_ids_that_are_correctly_translated(
                  'hi'), ['content'])
  
      def test_get_content_ids_that_are_correctly_translated_with_some_existing_translations_needs_update(self): # pylint: disable=line-too-long
          written_translations_dict = {
              'translations_mapping': {
                  'content': {
                      'hi': {
                          'data_format': 'html',
                          'translation': '<p>hello!</p>',
                          'needs_update': True
                      }
                  },
                  'hint_1': {}
              }
          }
          written_translations = state_domain.WrittenTranslations.from_dict(
              written_translations_dict)
  
          self.assertEqual(
              written_translations.get_content_ids_that_are_correctly_translated(
                  'hi'), [])
  
  
  class RecordedVoiceoversDomainUnitTests(test_utils.GenericTestBase):
      """Test methods operating on recorded voiceovers."""
  
      def test_from_and_to_dict_wroks_correctly(self):
          recorded_voiceovers_dict = {
              'voiceovers_mapping': {
                  'content1': {
                      'en': {
                          'filename': 'xyz.mp3',
                          'file_size_bytes': 123,
                          'needs_update': True,
                          'duration_secs': 1.1
                      },
                      'hi': {
                          'filename': 'abc.mp3',
                          'file_size_bytes': 1234,
                          'needs_update': False,
                          'duration_secs': 1.3
                      }
                  },
                  'feedback_1': {
                      'hi': {
                          'filename': 'xyz.mp3',
                          'file_size_bytes': 123,
                          'needs_update': False,
                          'duration_secs': 1.1
                      },
                      'en': {
                          'filename': 'xyz.mp3',
                          'file_size_bytes': 123,
                          'needs_update': False,
                          'duration_secs': 1.3
                      }
                  }
              }
          }
  
          recorded_voiceovers = state_domain.RecordedVoiceovers.from_dict(
              recorded_voiceovers_dict)
          self.assertEqual(
              recorded_voiceovers.to_dict(), recorded_voiceovers_dict)
  
      def test_get_content_ids_for_voiceovers_return_correct_list_of_content_id(self): # pylint: disable=line-too-long
          recorded_voiceovers = state_domain.RecordedVoiceovers.from_dict({
              'voiceovers_mapping': {}
          })
          self.assertEqual(
              recorded_voiceovers.get_content_ids_for_voiceovers(), [])
  
          recorded_voiceovers.add_content_id_for_voiceover('feedback_1')
          recorded_voiceovers.add_content_id_for_voiceover('feedback_2')
          self.assertItemsEqual(
              recorded_voiceovers.get_content_ids_for_voiceovers(),
              ['feedback_2', 'feedback_1'])
  
      def test_add_content_id_for_voiceovers_adds_content_id(self):
          recorded_voiceovers = state_domain.RecordedVoiceovers.from_dict({
              'voiceovers_mapping': {}
          })
  
          self.assertEqual(
              len(recorded_voiceovers.get_content_ids_for_voiceovers()), 0)
  
          new_content_id = 'content_id'
          recorded_voiceovers.add_content_id_for_voiceover(new_content_id)
  
          self.assertEqual(
              len(recorded_voiceovers.get_content_ids_for_voiceovers()), 1)
          self.assertEqual(
              recorded_voiceovers.get_content_ids_for_voiceovers(),
              ['content_id'])
  
      def test_add_content_id_for_voiceover_with_invalid_content_id_raise_error(
              self):
          recorded_voiceovers = state_domain.RecordedVoiceovers.from_dict({
              'voiceovers_mapping': {}
          })
          invalid_content_id = 123
          with self.assertRaisesRegexp(
              Exception, 'Expected content_id to be a string, received 123'):
              recorded_voiceovers.add_content_id_for_voiceover(
                  invalid_content_id)
  
      def test_add_content_id_for_voiceover_with_existing_content_id_raise_error( # pylint: disable=line-too-long
              self):
          recorded_voiceovers_dict = {
              'voiceovers_mapping': {
                  'feedback_1': {
                      'en': {
                          'filename': 'xyz.mp3',
                          'file_size_bytes': 123,
                          'needs_update': False,
                          'duration_secs': 1.1
                      }
                  }
              }
          }
  
          recorded_voiceovers = state_domain.RecordedVoiceovers.from_dict(
              recorded_voiceovers_dict)
          existing_content_id = 'feedback_1'
          with self.assertRaisesRegexp(
              Exception, 'The content_id feedback_1 already exist.'):
              recorded_voiceovers.add_content_id_for_voiceover(
                  existing_content_id)
  
      def test_delete_content_id_for_voiceovers_deletes_content_id(self):
          old_recorded_voiceovers_dict = {
              'voiceovers_mapping': {
                  'content': {
                      'en': {
                          'filename': 'xyz.mp3',
                          'file_size_bytes': 123,
                          'needs_update': False,
                          'duration_secs': 1.1
                      }
                  }
              }
          }
  
          recorded_voiceovers = state_domain.RecordedVoiceovers.from_dict(
              old_recorded_voiceovers_dict)
          self.assertEqual(
              len(recorded_voiceovers.get_content_ids_for_voiceovers()), 1)
  
          recorded_voiceovers.delete_content_id_for_voiceover('content')
  
          self.assertEqual(
              len(recorded_voiceovers.get_content_ids_for_voiceovers()), 0)
  
      def test_delete_content_id_for_voiceover_with_nonexisting_content_id_raise_error(self): # pylint: disable=line-too-long
          recorded_voiceovers_dict = {
              'voiceovers_mapping': {
                  'content': {}
              }
          }
          recorded_voiceovers = state_domain.RecordedVoiceovers.from_dict(
              recorded_voiceovers_dict)
          nonexisting_content_id_to_delete = 'feedback_1'
          with self.assertRaisesRegexp(
              Exception, 'The content_id feedback_1 does not exist.'):
              recorded_voiceovers.delete_content_id_for_voiceover(
                  nonexisting_content_id_to_delete)
  
      def test_delete_content_id_for_voiceover_with_invalid_content_id_raise_error(self): # pylint: disable=line-too-long
          recorded_voiceovers = state_domain.RecordedVoiceovers.from_dict({
              'voiceovers_mapping': {}
          })
          invalid_content_id_to_delete = 123
          with self.assertRaisesRegexp(
              Exception, 'Expected content_id to be a string, '):
              recorded_voiceovers.delete_content_id_for_voiceover(
                  invalid_content_id_to_delete)
  
      def test_validation_with_invalid_content_id_raise_error(self):
          recorded_voiceovers_dict = {
              'voiceovers_mapping': {
                  123: {}
              }
          }
  
          recorded_voiceovers = state_domain.RecordedVoiceovers.from_dict(
              recorded_voiceovers_dict)
  
          with self.assertRaisesRegexp(
              Exception, 'Expected content_id to be a string, '):
              recorded_voiceovers.validate([123])
  
      def test_validate_non_dict_language_code_to_voiceover(self):
          recorded_voiceovers = state_domain.RecordedVoiceovers({
              'en': []
          })
  
          with self.assertRaisesRegexp(
              Exception,
              re.escape('Expected content_id value to be a dict, received []')):
              recorded_voiceovers.validate(None)
  
      def test_validation_with_invalid_type_language_code_raise_error(self):
          recorded_voiceovers_dict = {
              'voiceovers_mapping': {
                  'content': {
                      123: {
                          'filename': 'xyz.mp3',
                          'file_size_bytes': 123,
                          'needs_update': False,
                          'duration_secs': 1.1
                      }
                  }
              }
          }
  
          recorded_voiceovers = state_domain.RecordedVoiceovers.from_dict(
              recorded_voiceovers_dict)
  
          with self.assertRaisesRegexp(
              Exception, 'Expected language_code to be a string, '):
              recorded_voiceovers.validate(['content'])
  
      def test_validation_with_unknown_language_code_raise_error(self):
          recorded_voiceovers_dict = {
              'voiceovers_mapping': {
                  'content': {
                      'ed': {
                          'filename': 'xyz.mp3',
                          'file_size_bytes': 123,
                          'needs_update': False,
                          'duration_secs': 1.1
                      }
                  }
              }
          }
  
          recorded_voiceovers = state_domain.RecordedVoiceovers.from_dict(
              recorded_voiceovers_dict)
  
          with self.assertRaisesRegexp(Exception, 'Invalid language_code: ed'):
              recorded_voiceovers.validate(['content'])
  
      def test_validation_with_invalid_content_id_list(self):
          recorded_voiceovers_dict = {
              'voiceovers_mapping': {
                  'content': {
                      'en': {
                          'filename': 'xyz.mp3',
                          'file_size_bytes': 123,
                          'needs_update': False,
                          'duration_secs': 1.1
                      }
                  }
              }
          }
  
          recorded_voiceovers = state_domain.RecordedVoiceovers.from_dict(
              recorded_voiceovers_dict)
  
          with self.assertRaisesRegexp(
              Exception,
              re.escape(
                  'Expected state recorded_voiceovers to match the listed '
                  'content ids [\'invalid_content\']')):
              recorded_voiceovers.validate(['invalid_content'])
  
  
  class VoiceoverDomainTests(test_utils.GenericTestBase):
  
      def setUp(self):
          super(VoiceoverDomainTests, self).setUp()
          self.voiceover = state_domain.Voiceover('filename.mp3', 10, False, 15.0)
  
      def test_validate_non_str_filename(self):
          self.voiceover.validate()
          self.voiceover.filename = 0
          with self.assertRaisesRegexp(
              Exception, 'Expected audio filename to be a string'):
              self.voiceover.validate()
  
      def test_validate_filename(self):
          self.voiceover.validate()
          self.voiceover.filename = 'invalid_filename'
          with self.assertRaisesRegexp(Exception, 'Invalid audio filename'):
              self.voiceover.validate()
  
      def test_validate_audio_extension(self):
          self.voiceover.validate()
          self.voiceover.filename = 'filename.png'
          with self.assertRaisesRegexp(
              Exception,
              re.escape(
                  'Invalid audio filename: it should have one of the following '
                  'extensions: %s'
                  % list(feconf.ACCEPTED_AUDIO_EXTENSIONS.keys()))):
              self.voiceover.validate()
  
      def test_validate_non_int_file_size_bytes(self):
          self.voiceover.validate()
          self.voiceover.file_size_bytes = 'file_size_bytes'
          with self.assertRaisesRegexp(
              Exception, 'Expected file size to be an int'):
              self.voiceover.validate()
  
      def test_validate_negative_file_size_bytes(self):
          self.voiceover.validate()
          self.voiceover.file_size_bytes = -1
          with self.assertRaisesRegexp(Exception, 'Invalid file size'):
              self.voiceover.validate()
  
      def test_validate_non_bool_needs_update(self):
          self.voiceover.validate()
          self.voiceover.needs_update = 'needs_update'
          with self.assertRaisesRegexp(
              Exception, 'Expected needs_update to be a bool'):
              self.voiceover.validate()
  
      def test_validate_float_duration_secs(self):
          self.voiceover.validate()
          self.voiceover.duration_secs = 'duration_secs'
          with self.assertRaisesRegexp(
              Exception, 'Expected duration_secs to be a float'):
              self.voiceover.validate()
  
      def test_validate_int_duration_secs(self):
          self.voiceover.validate()
          self.voiceover.duration_secs = 10
          with self.assertRaisesRegexp(
              Exception, 'Expected duration_secs to be a float'):
              self.voiceover.validate()
  
      def test_validate_negative_duration_seconds(self):
          self.voiceover.validate()
          self.voiceover.duration_secs = -1.45
          with self.assertRaisesRegexp(
              Exception, 'Expected duration_secs to be positive number, '
              'or zero if not yet specified'):
              self.voiceover.validate()
  