package kr.itn.itnhub.provision;

import kr.itn.itnhub.AbstractDbTest;
import kr.itn.itnhub.contact.Contact;
import kr.itn.itnhub.contact.ContactMapper;
import kr.itn.itnhub.mattermost.MattermostClient;
import kr.itn.itnhub.mattermost.MattermostException;
import kr.itn.itnhub.org.OrgStatus;
import kr.itn.itnhub.org.Organization;
import kr.itn.itnhub.org.OrganizationMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.jdbc.core.JdbcTemplate;

import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

class ChannelProvisionServiceTest extends AbstractDbTest {

    @Autowired
    ChannelProvisionService service;

    @SpyBean
    OrganizationMapper mapper;

    @Autowired
    ContactMapper contactMapper;

    @Autowired
    JdbcTemplate jdbc;

    @MockBean
    MattermostClient mattermost;

    private Long orgId;

    @BeforeEach
    void setUp() {
        jdbc.update("delete from organization");
        jdbc.update("delete from contact");

        Organization org = new Organization();
        org.setOrgNo("001");
        org.setOrgName("국제방송교류재단");
        org.setChannelSlug("001");
        mapper.upsertBySeed(org);

        orgId = mapper.findAll().get(0).getId();
        assignApplicant(orgId, "송민지");
    }

    private Long assignApplicant(Long id, String name) {
        Contact contact = new Contact();
        contact.setCategory("APPLICANT");
        contact.setName(name);
        contactMapper.insert(contact);
        mapper.updateAssignments(id, contact.getId(), null, null, null, null);
        return contact.getId();
    }

    @Test
    void 채널이_없으면_두_개를_생성하고_ID를_저장한다() {
        when(mattermost.findChannelIdByInternalName(anyString())).thenReturn(Optional.empty());
        when(mattermost.findChannelIdByDisplayName(anyString())).thenReturn(Optional.empty());
        when(mattermost.createPrivateChannel("org-001-mj", "001_국제방송교류재단 (문정원)"))
                .thenReturn("id-mj");
        when(mattermost.createPrivateChannel("org-001-law", "001_국제방송교류재단 (법률검토)"))
                .thenReturn("id-law");

        ProvisionResult result = service.provision(orgId);

        assertThat(result.mj()).isEqualTo(ProvisionOutcome.CREATED);
        assertThat(result.law()).isEqualTo(ProvisionOutcome.CREATED);

        Organization after = mapper.findById(orgId);
        assertThat(after.getChannelIdMj()).isEqualTo("id-mj");
        assertThat(after.getChannelIdLaw()).isEqualTo("id-law");
        assertThat(after.getStatus()).isEqualTo(OrgStatus.ACTIVE);
    }

    @Test
    void 이미_ID가_있으면_아무것도_생성하지_않는다() {
        mapper.updateChannelIdMj(orgId, "id-mj");
        mapper.updateChannelIdLaw(orgId, "id-law");

        ProvisionResult result = service.provision(orgId);

        assertThat(result.mj()).isEqualTo(ProvisionOutcome.CACHED);
        assertThat(result.law()).isEqualTo(ProvisionOutcome.CACHED);

        verify(mattermost, never()).createPrivateChannel(anyString(), anyString());
        verify(mattermost, never()).findChannelIdByInternalName(anyString());
    }

    @Test
    void DB에는_없고_서버에_내부명이_있으면_ID를_복구한다() {
        when(mattermost.findChannelIdByInternalName("org-001-mj"))
                .thenReturn(Optional.of("recovered-mj"));
        when(mattermost.findChannelIdByInternalName("org-001-law"))
                .thenReturn(Optional.of("recovered-law"));

        ProvisionResult result = service.provision(orgId);

        assertThat(result.mj()).isEqualTo(ProvisionOutcome.RECOVERED);
        assertThat(result.law()).isEqualTo(ProvisionOutcome.RECOVERED);

        verify(mattermost, never()).createPrivateChannel(anyString(), anyString());

        Organization after = mapper.findById(orgId);
        assertThat(after.getChannelIdMj()).isEqualTo("recovered-mj");
        assertThat(after.getChannelIdLaw()).isEqualTo("recovered-law");
    }

    @Test
    void 내부명으로_못_찾으면_표시명으로_복구를_시도한다() {
        when(mattermost.findChannelIdByInternalName(anyString())).thenReturn(Optional.empty());
        when(mattermost.findChannelIdByDisplayName("001_국제방송교류재단 (문정원)"))
                .thenReturn(Optional.of("bydisplay-mj"));
        when(mattermost.findChannelIdByDisplayName("001_국제방송교류재단 (법률검토)"))
                .thenReturn(Optional.empty());
        when(mattermost.createPrivateChannel(eq("org-001-law"), anyString()))
                .thenReturn("new-law");

        ProvisionResult result = service.provision(orgId);

        assertThat(result.mj()).isEqualTo(ProvisionOutcome.RECOVERED);
        assertThat(result.law()).isEqualTo(ProvisionOutcome.CREATED);

        verify(mattermost, never()).createPrivateChannel(eq("org-001-mj"), anyString());
    }

    @Test
    void 법률검토가_실패해도_문정원_ID는_저장된다() {
        when(mattermost.findChannelIdByInternalName(anyString())).thenReturn(Optional.empty());
        when(mattermost.findChannelIdByDisplayName(anyString())).thenReturn(Optional.empty());
        when(mattermost.createPrivateChannel(eq("org-001-mj"), anyString()))
                .thenReturn("id-mj");
        when(mattermost.createPrivateChannel(eq("org-001-law"), anyString()))
                .thenThrow(new MattermostException("서버 오류"));

        ProvisionResult result = service.provision(orgId);

        assertThat(result.mj()).isEqualTo(ProvisionOutcome.CREATED);
        assertThat(result.law()).isEqualTo(ProvisionOutcome.FAILED);
        assertThat(result.message()).contains("법률검토");

        Organization after = mapper.findById(orgId);
        assertThat(after.getChannelIdMj()).isEqualTo("id-mj");
        assertThat(after.getChannelIdLaw()).isNull();
        assertThat(after.getStatus()).isEqualTo(OrgStatus.PARTIAL);
    }

    @Test
    void 부분_실패_후_재시도는_없는_쪽만_생성한다() {
        mapper.updateChannelIdMj(orgId, "id-mj");

        when(mattermost.findChannelIdByInternalName("org-001-law")).thenReturn(Optional.empty());
        when(mattermost.findChannelIdByDisplayName(anyString())).thenReturn(Optional.empty());
        when(mattermost.createPrivateChannel(eq("org-001-law"), anyString()))
                .thenReturn("retry-law");

        ProvisionResult result = service.provision(orgId);

        assertThat(result.mj()).isEqualTo(ProvisionOutcome.CACHED);
        assertThat(result.law()).isEqualTo(ProvisionOutcome.CREATED);

        verify(mattermost, never()).createPrivateChannel(eq("org-001-mj"), anyString());
        assertThat(mapper.findById(orgId).getStatus()).isEqualTo(OrgStatus.ACTIVE);
    }

    @Test
    void 치안정책연구소는_합성코드_008_01로_내부명을_만든다() {
        Organization special = new Organization();
        special.setOrgNo("008_01");
        special.setOrgName("경찰청_치안정책연구소");
        special.setChannelSlug("008_01");
        mapper.upsertBySeed(special);

        Long specialId = mapper.findAll().stream()
                .filter(o -> "경찰청_치안정책연구소".equals(o.getOrgName()))
                .findFirst().orElseThrow().getId();
        assignApplicant(specialId, "담당자");

        when(mattermost.findChannelIdByInternalName(anyString())).thenReturn(Optional.empty());
        when(mattermost.findChannelIdByDisplayName(anyString())).thenReturn(Optional.empty());
        when(mattermost.createPrivateChannel(anyString(), anyString())).thenReturn("x");

        service.provision(specialId);

        verify(mattermost).createPrivateChannel(
                "org-008-01-mj", "008_01_경찰청_치안정책연구소 (문정원)");
        verify(mattermost).createPrivateChannel(
                "org-008-01-law", "008_01_경찰청_치안정책연구소 (법률검토)");
    }

    @Test
    void 신청기관_담당자가_배정되지_않으면_생성하지_않는다() {
        Organization pending = new Organization();
        pending.setOrgNo("099");
        pending.setOrgName("정보없는기관");
        pending.setChannelSlug("099");
        mapper.upsertBySeed(pending);

        Long pendingId = mapper.findAll().stream()
                .filter(o -> "정보없는기관".equals(o.getOrgName()))
                .findFirst().orElseThrow().getId();

        ProvisionResult result = service.provision(pendingId);

        assertThat(result.mj()).isEqualTo(ProvisionOutcome.FAILED);
        assertThat(result.law()).isEqualTo(ProvisionOutcome.FAILED);
        assertThat(result.message()).contains("신청기관 담당자");

        verify(mattermost, never()).createPrivateChannel(anyString(), anyString());
    }

    @Test
    void 문정원이_실패해도_법률검토는_시도된다() {
        when(mattermost.findChannelIdByInternalName(anyString())).thenReturn(Optional.empty());
        when(mattermost.findChannelIdByDisplayName(anyString())).thenReturn(Optional.empty());
        when(mattermost.createPrivateChannel(eq("org-001-mj"), anyString()))
                .thenThrow(new MattermostException("문정원 서버 오류"));
        when(mattermost.createPrivateChannel(eq("org-001-law"), anyString()))
                .thenReturn("id-law");

        ProvisionResult result = service.provision(orgId);

        assertThat(result.mj()).isEqualTo(ProvisionOutcome.FAILED);
        assertThat(result.law()).isEqualTo(ProvisionOutcome.CREATED);
        assertThat(result.message()).contains("문정원");

        Organization after = mapper.findById(orgId);
        assertThat(after.getChannelIdMj()).isNull();
        assertThat(after.getChannelIdLaw()).isEqualTo("id-law");
        assertThat(after.getStatus()).isEqualTo(OrgStatus.PARTIAL);
    }

    @Test
    void 양쪽_다_실패하면_메시지에_두_채널_라벨이_모두_누적된다() {
        when(mattermost.findChannelIdByInternalName(anyString())).thenReturn(Optional.empty());
        when(mattermost.findChannelIdByDisplayName(anyString())).thenReturn(Optional.empty());
        when(mattermost.createPrivateChannel(eq("org-001-mj"), anyString()))
                .thenThrow(new MattermostException("문정원 서버 오류"));
        when(mattermost.createPrivateChannel(eq("org-001-law"), anyString()))
                .thenThrow(new MattermostException("법률검토 서버 오류"));

        ProvisionResult result = service.provision(orgId);

        assertThat(result.mj()).isEqualTo(ProvisionOutcome.FAILED);
        assertThat(result.law()).isEqualTo(ProvisionOutcome.FAILED);
        assertThat(result.message()).contains("문정원").contains("법률검토");

        Organization after = mapper.findById(orgId);
        assertThat(after.getChannelIdMj()).isNull();
        assertThat(after.getChannelIdLaw()).isNull();
    }

    /**
     * Finding 1 회귀 테스트: MattermostClient 호출은 성공했는데 그 직후 DB 기록(mapper)이
     * DataAccessException 계열로 실패하는 경우. 수정 전에는 ensure()가 MattermostException만
     * catch 하므로 이 예외가 provision() 밖으로 전파되어 두 번째(법률검토) 채널은 시도조차
     * 되지 않고 호출자는 ProvisionResult를 받지 못한다. 수정 후에는 해당 채널만 FAILED로
     * 강등되고 나머지 채널은 정상적으로 계속 시도되어야 한다.
     */
    @Test
    void 채널_생성_성공_직후_DB_저장_실패는_예외로_전파되지_않고_FAILED로_처리된다() {
        when(mattermost.findChannelIdByInternalName(anyString())).thenReturn(Optional.empty());
        when(mattermost.findChannelIdByDisplayName(anyString())).thenReturn(Optional.empty());
        when(mattermost.createPrivateChannel(eq("org-001-mj"), anyString()))
                .thenReturn("id-mj");
        when(mattermost.createPrivateChannel(eq("org-001-law"), anyString()))
                .thenReturn("id-law");

        doThrow(new DataIntegrityViolationException("DB 저장 실패"))
                .when(mapper).updateChannelIdMj(eq(orgId), anyString());

        ProvisionResult result = service.provision(orgId);

        assertThat(result.mj()).isEqualTo(ProvisionOutcome.FAILED);
        assertThat(result.law()).isEqualTo(ProvisionOutcome.CREATED);
        assertThat(result.message()).contains("문정원");

        Organization after = mapper.findById(orgId);
        assertThat(after.getChannelIdMj()).isNull();
        assertThat(after.getChannelIdLaw()).isEqualTo("id-law");
    }

    @Test
    void 채널_2개가_처음_모두_갖춰지면_1단계로_자동_시작하고_이력이_한_행_남는다() {
        when(mattermost.findChannelIdByInternalName(anyString())).thenReturn(Optional.empty());
        when(mattermost.findChannelIdByDisplayName(anyString())).thenReturn(Optional.empty());
        when(mattermost.createPrivateChannel(eq("org-001-mj"), anyString())).thenReturn("id-mj");
        when(mattermost.createPrivateChannel(eq("org-001-law"), anyString())).thenReturn("id-law");

        service.provision(orgId);

        Organization after = mapper.findById(orgId);
        assertThat(after.getStage()).isEqualTo(1);
        assertThat(jdbc.queryForObject(
                "select count(*) from stage_history where org_id = ?", Integer.class, orgId))
                .isEqualTo(1);
    }

    @Test
    void 이미_단계가_있는_기관은_채널_재확인으로_단계가_바뀌지_않는다() {
        mapper.updateChannelIdMj(orgId, "id-mj");
        mapper.updateChannelIdLaw(orgId, "id-law");
        mapper.updateStage(orgId, 5);

        service.provision(orgId);

        Organization after = mapper.findById(orgId);
        assertThat(after.getStage()).isEqualTo(5);
        assertThat(jdbc.queryForObject(
                "select count(*) from stage_history where org_id = ?", Integer.class, orgId))
                .isEqualTo(0);
    }

    @Test
    void 이미_채널이_있어_CACHED만_반환되는_재실행은_이력을_추가하지_않는다() {
        mapper.updateChannelIdMj(orgId, "id-mj");
        mapper.updateChannelIdLaw(orgId, "id-law");

        // 첫 provision으로 자동 1단계 시작 + 이력 1건.
        service.provision(orgId);
        assertThat(jdbc.queryForObject(
                "select count(*) from stage_history where org_id = ?", Integer.class, orgId))
                .isEqualTo(1);

        // 다시 호출해도(CACHED만 반환) 이력은 늘지 않는다.
        ProvisionResult result = service.provision(orgId);

        assertThat(result.mj()).isEqualTo(ProvisionOutcome.CACHED);
        assertThat(result.law()).isEqualTo(ProvisionOutcome.CACHED);
        assertThat(jdbc.queryForObject(
                "select count(*) from stage_history where org_id = ?", Integer.class, orgId))
                .isEqualTo(1);
    }
}
