from git import Repo, Head
from rspec_tools.errors import InvalidArgumentError
from pathlib import Path
from typing import Optional
from unittest.mock import Mock, patch
import pytest

from rspec_tools.create_rule import RuleCreator, create_new_rule

@pytest.fixture
def git_config():
  '''Create a mock git configuration.'''
  return {
    'user.name': 'testuser',
    'user.email': 'testuser@mock.mock'
  }

@pytest.fixture
def mock_rspec_repo(tmpdir):
  repo_dir = tmpdir.mkdir("mock_rspec")
  repo = Repo.init(str(repo_dir))
  repo.init()

  with repo.config_writer() as config_writer:
    config_writer.set_value('user', 'name', 'originuser')
    config_writer.set_value('user', 'email', 'originuser@mock.mock')

  rules_dir = repo_dir.mkdir('rules')
  # create a file just to have a "rules" directory
  gitignore = rules_dir.join('.gitignore')
  gitignore.ensure()
  repo.index.add([str(gitignore)])
  repo.index.commit('init rules')

  # Create the id counter branch. Note that it is an orphan branch.
  repo.head.reference = Head(repo, f'refs/heads/{RuleCreator.ID_COUNTER_BRANCH}')
  repo.git.reset('--hard')
  counter_file = repo_dir.join(RuleCreator.ID_COUNTER_FILENAME)
  counter_file.write('0')
  repo.index.add([str(counter_file)])
  commit = repo.index.commit('init counter', parent_commits=None)

  # Checkout a specific commit so that the repo can be pushed to without
  # making the index and work tree inconsistent.
  repo.git.checkout(commit.hexsha)

  return repo

@pytest.fixture
def rule_creator(tmpdir, mock_rspec_repo: Repo, git_config: dict[str, str]):
  cloned_repo = tmpdir.mkdir("cloned_repo")
  return RuleCreator(mock_rspec_repo.working_dir, str(cloned_repo), git_config)


def test_reserve_rule_number_simple(rule_creator: RuleCreator, mock_rspec_repo: Repo):
  '''Test that RuleCreator.reserve_rule_id() increments the id and returns the old value.'''
  assert rule_creator.reserve_rule_number() == 0

  assert read_counter_file(mock_rspec_repo) == '1'


def test_reserve_rule_number_parallel_reservations(tmpdir, mock_rspec_repo: Repo, git_config):
  '''Test that RuleCreator.reserve_rule_id() works when multiple reservations are done in parallel.'''
  cloned_repo1 = tmpdir.mkdir("cloned_repo1")
  rule_creator1 = RuleCreator(mock_rspec_repo.working_dir, str(cloned_repo1), git_config)
  cloned_repo2 = tmpdir.mkdir("cloned_repo2")
  rule_creator2 = RuleCreator(mock_rspec_repo.working_dir, str(cloned_repo2), git_config)

  assert rule_creator1.reserve_rule_number() == 0
  assert rule_creator2.reserve_rule_number() == 1
  assert rule_creator1.reserve_rule_number() == 2

  assert read_counter_file(mock_rspec_repo) == '3'


def read_counter_file(repo):
  '''Reads the counter file from the provided repository and returns its content.'''
  repo.git.checkout(RuleCreator.ID_COUNTER_BRANCH)
  counter_path = Path(repo.working_dir).joinpath(RuleCreator.ID_COUNTER_FILENAME)
  return counter_path.read_text()


def test_create_new_multi_lang_rule_branch(rule_creator: RuleCreator, mock_rspec_repo: Repo):
  '''Test create_new_rule_branch for a multi-language rule.'''
  rule_number = rule_creator.reserve_rule_number()

  languages = ['java', 'javascript']
  branch = rule_creator.create_new_rule_branch(rule_number, languages)

  # Check that the branch was pushed successfully to the origin
  mock_rspec_repo.git.checkout(branch)
  rule_dir = Path(mock_rspec_repo.working_dir).joinpath('rules', f'S{rule_number}')
  assert rule_dir.exists()

  common_root = rule_creator.TEMPLATE_PATH.joinpath('multi_language', 'common')
  for common_item in common_root.glob('**/*'):
    if common_item.is_file():
      expected_content = common_item.read_text().replace('${RSPEC_ID}', str(rule_number))
      relative_path = common_item.relative_to(common_root)
      actual_content = rule_dir.joinpath(relative_path).read_text()
      assert actual_content == expected_content

  lang_root = rule_creator.TEMPLATE_PATH.joinpath('multi_language', 'language_specific')
  for lang in languages:
    for lang_item in lang_root.glob('**/*'):
      if lang_item.is_file():
        expected_content = lang_item.read_text().replace('${RSPEC_ID}', str(rule_number))
        relative_path = lang_item.relative_to(lang_root)
        actual_content = rule_dir.joinpath(lang, relative_path).read_text()
        assert actual_content == expected_content

def test_create_new_single_lang_rule_branch(rule_creator: RuleCreator, mock_rspec_repo: Repo):
  '''Test create_new_rule_branch for a single-language rule.'''
  rule_number = rule_creator.reserve_rule_number()

  languages = ['cfamily']
  branch = rule_creator.create_new_rule_branch(rule_number, languages)

  # Check that the branch was pushed successfully to the origin
  mock_rspec_repo.git.checkout(branch)
  rule_dir = Path(mock_rspec_repo.working_dir).joinpath('rules', f'S{rule_number}')
  assert rule_dir.exists()

  common_root = rule_creator.TEMPLATE_PATH.joinpath('single_language', 'common')
  for common_item in common_root.glob('**/*'):
    if common_item.is_file():
      expected_content = common_item.read_text().replace('${RSPEC_ID}', str(rule_number))
      relative_path = common_item.relative_to(common_root)
      actual_content = rule_dir.joinpath(relative_path).read_text()
      assert actual_content == expected_content

  lang_root = rule_creator.TEMPLATE_PATH.joinpath('single_language', 'language_specific')
  for lang in languages:
    for lang_item in lang_root.glob('**/*'):
      if lang_item.is_file():
        expected_content = lang_item.read_text().replace('${RSPEC_ID}', str(rule_number))
        relative_path = lang_item.relative_to(lang_root)
        actual_content = rule_dir.joinpath(lang, relative_path).read_text()
        assert actual_content == expected_content

def test_create_new_rule_pr(rule_creator: RuleCreator):
  '''Test create_new_rule_branch adds the right user and labels.'''
  rule_number = rule_creator.reserve_rule_number()
  languages = ['cfamily']

  ghMock = Mock()
  ghRepoMock = Mock()
  pullMock = Mock()
  ghRepoMock.create_pull = Mock(return_value=pullMock)
  ghMock.get_repo = Mock(return_value=ghRepoMock)
  def mockGithub(user: Optional[str]):
    return ghMock

  rule_creator.create_new_rule_pull_request(mockGithub, rule_number, languages, ['mylab', 'other-lab'], user='testuser')

  ghRepoMock.create_pull.assert_called_once();
  assert ghRepoMock.create_pull.call_args.kwargs['title'].startswith('Create rule S')
  pullMock.add_to_assignees.assert_called_with('testuser');
  pullMock.add_to_labels.assert_called_with('mylab', 'other-lab');

@patch('rspec_tools.create_rule.RuleCreator')
def test_create_new_rule(mockRuleCreator):
  mockRuleCreator.return_value = Mock()
  mockRuleCreator.return_value.create_new_rule_pull_request = Mock()
  prMock = mockRuleCreator.return_value.create_new_rule_pull_request
  create_new_rule('cfamily,php', 'my token', 'testuser')
  prMock.assert_called_once()
  assert set(prMock.call_args.args[2]) == set(['cfamily', 'php'])
  assert set(prMock.call_args.args[3]) == set(['cfamily', 'php'])

@patch('rspec_tools.create_rule.RuleCreator')
def test_create_new_rule_unsupported_language(mockRuleCreator):
  mockRuleCreator.return_value = Mock()
  mockRuleCreator.return_value.create_new_rule_pull_request = Mock()
  prMock = mockRuleCreator.return_value.create_new_rule_pull_request
  with pytest.raises(InvalidArgumentError):
    create_new_rule('russian,php', 'my token', 'testuser')
