"""Tests for update_issue.py module."""

from datetime import datetime
from pathlib import Path
from unittest.mock import Mock, mock_open, patch

import pytest
import typer
import yaml

from jps_jira_utils.update_issue import (
    add_business_days,
    calculate_next_friday,
    find_latest_sprint,
    get_current_user_account_id,
    get_field_id_by_name,
    get_issue_details,
    get_sprints,
    load_config,
    main,
    update_issue_fields,
)


class TestLoadConfig:
    """Tests for load_config function."""

    def test_load_config_success(self, tmp_path):
        """Test successful config loading."""
        config_file = tmp_path / "config.yaml"
        config_data = {
            "project_key": "TEST",
            "board_id": 123,
            "program": "MyProgram",
        }
        config_file.write_text(yaml.dump(config_data))

        result = load_config(config_file)

        assert result["project_key"] == "TEST"
        assert result["board_id"] == 123
        assert result["program"] == "MyProgram"

    def test_load_config_file_not_found(self):
        """Test when config file doesn't exist."""
        with pytest.raises(typer.Exit):
            load_config(Path("/nonexistent/config.yaml"))

    def test_load_config_invalid_yaml(self, tmp_path):
        """Test with invalid YAML content."""
        config_file = tmp_path / "config.yaml"
        config_file.write_text("invalid: yaml: content: {")

        with pytest.raises(typer.Exit):
            load_config(config_file)

    def test_load_config_empty_file(self, tmp_path):
        """Test with empty config file."""
        config_file = tmp_path / "config.yaml"
        config_file.write_text("")

        result = load_config(config_file)
        assert result == {}


class TestCalculateNextFriday:
    """Tests for calculate_next_friday function."""

    def test_calculate_next_friday_from_monday(self):
        """Test calculating Friday from Monday."""
        # Monday, January 6, 2025
        start = datetime(2025, 1, 6)
        result = calculate_next_friday(start, 5)
        
        # Should be Friday after 5 business days
        assert result == "2025-01-17"

    def test_calculate_next_friday_from_friday(self):
        """Test calculating Friday from Friday."""
        # Friday, January 3, 2025
        start = datetime(2025, 1, 3)
        result = calculate_next_friday(start, 3)
        
        # 3 business days = Mon, Tue, Wed, next Friday is 01-10
        assert result == "2025-01-10"

    def test_calculate_next_friday_zero_days(self):
        """Test with zero business days."""
        # Monday, January 6, 2025
        start = datetime(2025, 1, 6)
        result = calculate_next_friday(start, 0)
        
        # Should be the next Friday
        assert result == "2025-01-10"

    def test_calculate_next_friday_spans_weekend(self):
        """Test calculation that spans a weekend."""
        # Thursday, January 2, 2025
        start = datetime(2025, 1, 2)
        result = calculate_next_friday(start, 2)
        
        # 2 business days = Fri, Mon, next Friday is 01-10
        assert result == "2025-01-10"


class TestAddBusinessDays:
    """Tests for add_business_days function."""

    def test_add_business_days_weekdays(self):
        """Test adding business days during weekdays."""
        result = add_business_days("2025-01-06", 5)  # Monday
        assert result == "2025-01-13"  # Monday + 5 business days

    def test_add_business_days_spans_weekend(self):
        """Test adding days that span a weekend."""
        result = add_business_days("2025-01-03", 3)  # Friday
        # Fri + 3 business days = Mon, Tue, Wed
        assert result == "2025-01-08"

    def test_add_business_days_zero(self):
        """Test adding zero business days."""
        result = add_business_days("2025-01-06", 0)
        assert result == "2025-01-06"

    def test_add_business_days_from_weekend(self):
        """Test starting from Saturday."""
        # If we start from Sat, we add business days from that point
        result = add_business_days("2025-01-04", 1)  # Saturday
        assert result == "2025-01-06"  # Next Monday (1 business day)


class TestGetIssueDetails:
    """Tests for get_issue_details function."""

    @patch("jps_jira_utils.update_issue.setup_logging")
    def test_get_issue_details_success(self, mock_logging):
        """Test successful issue retrieval."""
        mock_logger = Mock()
        mock_logging.return_value = mock_logger

        session = Mock()
        response = Mock()
        response.ok = True
        response.json.return_value = {
            "key": "TEST-123",
            "fields": {"summary": "Test issue"},
        }
        session.get.return_value = response

        result = get_issue_details(session, "https://jira.example.com", "TEST-123", mock_logger)

        assert result["key"] == "TEST-123"
        assert result["fields"]["summary"] == "Test issue"

    @patch("jps_jira_utils.update_issue.setup_logging")
    def test_get_issue_details_not_found(self, mock_logging):
        """Test when issue is not found."""
        mock_logger = Mock()
        mock_logging.return_value = mock_logger

        session = Mock()
        response = Mock()
        response.ok = False
        response.status_code = 404
        response.text = "Not found"
        session.get.return_value = response

        with pytest.raises(typer.Exit):
            get_issue_details(session, "https://jira.example.com", "TEST-123", mock_logger)


class TestGetFieldIdByName:
    """Tests for get_field_id_by_name function."""

    @patch("jps_jira_utils.update_issue.setup_logging")
    def test_get_field_id_by_name_found(self, mock_logging):
        """Test finding field by name."""
        mock_logger = Mock()
        mock_logging.return_value = mock_logger

        session = Mock()
        response = Mock()
        response.ok = True
        response.json.return_value = [
            {"id": "customfield_10001", "name": "Software Component"},
            {"id": "customfield_10002", "name": "PROGRAM"},
        ]
        session.get.return_value = response

        result = get_field_id_by_name(
            session, "https://jira.example.com", "Software Component", mock_logger
        )

        assert result == "customfield_10001"

    @patch("jps_jira_utils.update_issue.setup_logging")
    def test_get_field_id_by_name_not_found(self, mock_logging):
        """Test when field is not found."""
        mock_logger = Mock()
        mock_logging.return_value = mock_logger

        session = Mock()
        response = Mock()
        response.ok = True
        response.json.return_value = [
            {"id": "customfield_10001", "name": "Other Field"},
        ]
        session.get.return_value = response

        result = get_field_id_by_name(
            session, "https://jira.example.com", "Nonexistent Field", mock_logger
        )

        assert result is None

    @patch("jps_jira_utils.update_issue.setup_logging")
    def test_get_field_id_by_name_case_insensitive(self, mock_logging):
        """Test case-insensitive field name matching."""
        mock_logger = Mock()
        mock_logging.return_value = mock_logger

        session = Mock()
        response = Mock()
        response.ok = True
        response.json.return_value = [
            {"id": "customfield_10001", "name": "Software Component"},
        ]
        session.get.return_value = response

        result = get_field_id_by_name(
            session, "https://jira.example.com", "software component", mock_logger
        )

        assert result == "customfield_10001"


class TestGetSprints:
    """Tests for get_sprints function."""

    @patch("jps_jira_utils.update_issue.setup_logging")
    def test_get_sprints_success(self, mock_logging):
        """Test successful sprint retrieval."""
        mock_logger = Mock()
        mock_logging.return_value = mock_logger

        session = Mock()
        response = Mock()
        response.ok = True
        response.json.return_value = {
            "values": [
                {"id": 1, "name": "Sprint 1"},
                {"id": 2, "name": "Sprint 2"},
            ]
        }
        session.get.return_value = response

        result = get_sprints(session, "https://jira.example.com", 123, mock_logger)

        assert len(result) == 2
        assert result[0]["name"] == "Sprint 1"

    @patch("jps_jira_utils.update_issue.setup_logging")
    def test_get_sprints_api_error(self, mock_logging):
        """Test when API returns error."""
        mock_logger = Mock()
        mock_logging.return_value = mock_logger

        session = Mock()
        response = Mock()
        response.ok = False
        response.status_code = 403
        response.text = "Forbidden"
        session.get.return_value = response

        result = get_sprints(session, "https://jira.example.com", 123, mock_logger)

        assert result == []


class TestFindLatestSprint:
    """Tests for find_latest_sprint function."""

    @patch("jps_jira_utils.update_issue.setup_logging")
    def test_find_latest_sprint_found(self, mock_logging):
        """Test finding latest sprint."""
        mock_logger = Mock()
        mock_logging.return_value = mock_logger

        sprints = [
            {"id": 1, "name": "COMPBIO FY25 Sprint 1"},
            {"id": 3, "name": "COMPBIO FY25 Sprint 3"},
            {"id": 2, "name": "COMPBIO FY25 Sprint 2"},
            {"id": 4, "name": "OTHER Sprint 1"},
        ]

        result = find_latest_sprint(sprints, "COMPBIO", mock_logger)

        assert result["id"] == 3
        assert result["name"] == "COMPBIO FY25 Sprint 3"

    @patch("jps_jira_utils.update_issue.setup_logging")
    def test_find_latest_sprint_none_matching(self, mock_logging):
        """Test when no sprints match."""
        mock_logger = Mock()
        mock_logging.return_value = mock_logger

        sprints = [
            {"id": 1, "name": "OTHER Sprint 1"},
            {"id": 2, "name": "DIFFERENT Sprint 2"},
        ]

        result = find_latest_sprint(sprints, "COMPBIO", mock_logger)

        assert result is None

    @patch("jps_jira_utils.update_issue.setup_logging")
    def test_find_latest_sprint_empty_list(self, mock_logging):
        """Test with empty sprint list."""
        mock_logger = Mock()
        mock_logging.return_value = mock_logger

        result = find_latest_sprint([], "COMPBIO", mock_logger)

        assert result is None


class TestGetCurrentUserAccountId:
    """Tests for get_current_user_account_id function."""

    @patch("jps_jira_utils.update_issue.setup_logging")
    def test_get_current_user_account_id_success(self, mock_logging):
        """Test successful user account ID retrieval."""
        mock_logger = Mock()
        mock_logging.return_value = mock_logger

        session = Mock()
        response = Mock()
        response.ok = True
        response.json.return_value = {
            "accountId": "account-123",
            "emailAddress": "user@example.com",
        }
        session.get.return_value = response

        result = get_current_user_account_id(session, "https://jira.example.com", mock_logger)

        assert result == "account-123"

    @patch("jps_jira_utils.update_issue.setup_logging")
    def test_get_current_user_account_id_error(self, mock_logging):
        """Test when API returns error."""
        mock_logger = Mock()
        mock_logging.return_value = mock_logger

        session = Mock()
        response = Mock()
        response.ok = False
        response.status_code = 401
        response.text = "Unauthorized"
        session.get.return_value = response

        with pytest.raises(typer.Exit):
            get_current_user_account_id(session, "https://jira.example.com", mock_logger)


class TestUpdateIssueFields:
    """Tests for update_issue_fields function."""

    @patch("jps_jira_utils.update_issue.setup_logging")
    def test_update_issue_fields_success(self, mock_logging):
        """Test successful field update."""
        mock_logger = Mock()
        mock_logging.return_value = mock_logger

        session = Mock()
        response = Mock()
        response.ok = True
        session.put.return_value = response

        updates = {
            "customfield_10001": {"value": "Component"},
            "duedate": "2025-01-17",
        }

        update_issue_fields(
            session, "https://jira.example.com", "TEST-123", updates, mock_logger
        )

        assert session.put.called
        call_args = session.put.call_args
        assert call_args.kwargs["json"]["fields"] == updates

    @patch("jps_jira_utils.update_issue.setup_logging")
    def test_update_issue_fields_empty_updates(self, mock_logging):
        """Test with no updates to make."""
        mock_logger = Mock()
        mock_logging.return_value = mock_logger

        session = Mock()

        update_issue_fields(
            session, "https://jira.example.com", "TEST-123", {}, mock_logger
        )

        assert not session.put.called

    @patch("jps_jira_utils.update_issue.setup_logging")
    def test_update_issue_fields_api_error(self, mock_logging):
        """Test when API returns error."""
        mock_logger = Mock()
        mock_logging.return_value = mock_logger

        session = Mock()
        response = Mock()
        response.ok = False
        response.status_code = 400
        response.text = "Bad Request"
        response.json.return_value = {"errorMessages": ["Bad Request"], "errors": {}}
        session.put.return_value = response

        updates = {"duedate": "2025-01-17"}

        with pytest.raises(typer.Exit):
            update_issue_fields(
                session, "https://jira.example.com", "TEST-123", updates, mock_logger
            )


class TestMainIntegration:
    """Integration tests for main function."""

    @patch("jps_jira_utils.update_issue.setup_logging")
    @patch("jps_jira_utils.update_issue.load_jira_env")
    @patch("jps_jira_utils.update_issue.load_config")
    @patch("jps_jira_utils.update_issue.requests.Session")
    @patch("jps_jira_utils.update_issue.get_current_user_account_id")
    @patch("jps_jira_utils.update_issue.get_field_id_by_name")
    def test_main_dry_run(
        self,
        mock_get_field,
        mock_get_user,
        mock_session,
        mock_config,
        mock_env,
        mock_logging,
        tmp_path,
    ):
        """Test main function in dry-run mode."""
        # Setup mocks
        mock_env.return_value = ("https://jira.example.com", "user@example.com", "token")
        mock_logger = Mock()
        mock_logging.return_value = mock_logger
        mock_config.return_value = {
            "project_key": "TEST",
            "program": "MyProgram",
            "default_due_date_business_days": 10,
        }
        mock_get_user.return_value = "account-123"
        mock_get_field.return_value = "customfield_10001"

        session = Mock()
        get_response = Mock()
        get_response.ok = True
        get_response.json.return_value = {
            "key": "TEST-123",
            "fields": {},  # All fields empty
        }
        session.get.return_value = get_response
        mock_session.return_value = session

        # Create temp config
        config_file = tmp_path / "config.yaml"
        config_file.write_text(yaml.dump({"project_key": "TEST"}))

        # Run main in dry-run mode
        main("TEST-123", config_file, None, None, True, None)

        # Verify no PUT request was made
        assert not session.put.called

    @patch("jps_jira_utils.update_issue.setup_logging")
    @patch("jps_jira_utils.update_issue.load_jira_env")
    @patch("jps_jira_utils.update_issue.load_config")
    @patch("jps_jira_utils.update_issue.requests.Session")
    @patch("jps_jira_utils.update_issue.get_current_user_account_id")
    @patch("jps_jira_utils.update_issue.get_field_id_by_name")
    def test_main_no_updates_needed(
        self,
        mock_get_field,
        mock_get_user,
        mock_session,
        mock_config,
        mock_env,
        mock_logging,
        tmp_path,
    ):
        """Test when all fields are already populated."""
        # Setup mocks
        mock_env.return_value = ("https://jira.example.com", "user@example.com", "token")
        mock_logger = Mock()
        mock_logging.return_value = mock_logger
        mock_config.return_value = {"project_key": "TEST"}
        mock_get_user.return_value = "account-123"
        mock_get_field.return_value = "customfield_10001"

        session = Mock()
        get_response = Mock()
        get_response.ok = True
        get_response.json.return_value = {
            "key": "TEST-123",
            "fields": {
                "duedate": "2025-12-31",
                "assignee": {"accountId": "account-456"},
                "customfield_10001": {"value": "Existing"},
            },
        }
        session.get.return_value = get_response
        mock_session.return_value = session

        config_file = tmp_path / "config.yaml"
        config_file.write_text(yaml.dump({"project_key": "TEST"}))

        # Run main
        main("TEST-123", config_file, None, None, False, None)

        # Verify no PUT request was made
        assert not session.put.called

    @patch("jps_jira_utils.update_issue.setup_logging")
    @patch("jps_jira_utils.update_issue.load_jira_env")
    @patch("jps_jira_utils.update_issue.load_config")
    @patch("jps_jira_utils.update_issue.requests.Session")
    @patch("jps_jira_utils.update_issue.get_current_user_account_id")
    @patch("jps_jira_utils.update_issue.get_field_id_by_name")
    def test_main_with_updates(
        self,
        mock_get_field,
        mock_get_user,
        mock_session,
        mock_config,
        mock_env,
        mock_logging,
        tmp_path,
        monkeypatch,
    ):
        """Test main function with actual updates."""
        # Setup mocks
        mock_env.return_value = ("https://jira.example.com", "user@example.com", "token")
        mock_logger = Mock()
        mock_logging.return_value = mock_logger
        mock_config.return_value = {
            "project_key": "TEST",
            "program": "MyProgram",
            "default_due_date_business_days": 5,
        }
        mock_get_user.return_value = "account-123"
        mock_get_field.return_value = "customfield_10001"

        session = Mock()
        get_response = Mock()
        get_response.ok = True
        get_response.json.return_value = {
            "key": "TEST-123",
            "fields": {},  # All fields empty
        }
        put_response = Mock()
        put_response.ok = True
        session.get.return_value = get_response
        session.put.return_value = put_response
        mock_session.return_value = session

        config_file = tmp_path / "config.yaml"
        config_file.write_text(yaml.dump({"project_key": "TEST"}))

        # Mock user confirmation
        monkeypatch.setattr("typer.confirm", lambda *args, **kwargs: True)

        # Run main
        main("TEST-123", config_file, None, None, False, None)

        # Verify PUT request was made
        assert session.put.called
