package net.siegeln.cameleer.saas.certificate;

import net.siegeln.cameleer.saas.tenant.TenantRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.time.Instant;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class CertificateServiceTest {

    @Mock
    private CertificateManager certManager;

    @Mock
    private CertificateRepository certRepository;

    @Mock
    private TenantRepository tenantRepository;

    private CertificateService service;

    @BeforeEach
    void setUp() {
        service = new CertificateService(certManager, certRepository, tenantRepository);
    }

    @Test
    void stage_delegatesToManagerAndSavesEntity() {
        var info = new CertificateInfo("CN=test", "CN=test", Instant.now(),
            Instant.now().plusSeconds(86400), false, true, "AA:BB");
        when(certManager.isAvailable()).thenReturn(true);
        when(certManager.stage(any(), any(), any()))
            .thenReturn(CertValidationResult.ok(info));
        when(certRepository.findByStatus(CertificateEntity.Status.STAGED))
            .thenReturn(Optional.empty());
        when(certRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        var result = service.stage("cert".getBytes(), "key".getBytes(), null, UUID.randomUUID());

        assertThat(result.valid()).isTrue();
        var captor = ArgumentCaptor.forClass(CertificateEntity.class);
        verify(certRepository).save(captor.capture());
        assertThat(captor.getValue().getStatus()).isEqualTo(CertificateEntity.Status.STAGED);
        assertThat(captor.getValue().getSubject()).isEqualTo("CN=test");
    }

    @Test
    void stage_discardsExistingStagedFirst() {
        var existing = new CertificateEntity();
        when(certManager.isAvailable()).thenReturn(true);
        when(certRepository.findByStatus(CertificateEntity.Status.STAGED))
            .thenReturn(Optional.of(existing));
        when(certManager.stage(any(), any(), any()))
            .thenReturn(CertValidationResult.fail(List.of("bad cert")));

        service.stage("cert".getBytes(), "key".getBytes(), null, UUID.randomUUID());

        verify(certRepository).delete(existing);
    }

    @Test
    void stage_returnsErrorWhenManagerUnavailable() {
        when(certManager.isAvailable()).thenReturn(false);

        var result = service.stage("cert".getBytes(), "key".getBytes(), null, UUID.randomUUID());

        assertThat(result.valid()).isFalse();
        assertThat(result.errors()).contains("Certificate management is not available");
    }

    @Test
    void activate_promotesStagedToActive() {
        var staged = new CertificateEntity();
        staged.setStatus(CertificateEntity.Status.STAGED);
        staged.setSubject("CN=new");

        when(certRepository.findByStatus(CertificateEntity.Status.STAGED))
            .thenReturn(Optional.of(staged));
        when(certRepository.findByStatus(CertificateEntity.Status.ARCHIVED))
            .thenReturn(Optional.empty());
        when(certRepository.findByStatus(CertificateEntity.Status.ACTIVE))
            .thenReturn(Optional.empty());

        service.activate();

        verify(certManager).activate();
        assertThat(staged.getStatus()).isEqualTo(CertificateEntity.Status.ACTIVE);
        assertThat(staged.getActivatedAt()).isNotNull();
    }

    @Test
    void activate_archivesCurrentActive() {
        var staged = new CertificateEntity();
        staged.setStatus(CertificateEntity.Status.STAGED);

        var active = new CertificateEntity();
        active.setStatus(CertificateEntity.Status.ACTIVE);

        when(certRepository.findByStatus(CertificateEntity.Status.STAGED))
            .thenReturn(Optional.of(staged));
        when(certRepository.findByStatus(CertificateEntity.Status.ARCHIVED))
            .thenReturn(Optional.empty());
        when(certRepository.findByStatus(CertificateEntity.Status.ACTIVE))
            .thenReturn(Optional.of(active));

        service.activate();

        assertThat(active.getStatus()).isEqualTo(CertificateEntity.Status.ARCHIVED);
        assertThat(active.getArchivedAt()).isNotNull();
    }

    @Test
    void activate_failsWithNoStagedCert() {
        when(certRepository.findByStatus(CertificateEntity.Status.STAGED))
            .thenReturn(Optional.empty());

        assertThatThrownBy(() -> service.activate())
            .isInstanceOf(IllegalStateException.class);
    }

    @Test
    void restore_swapsActiveAndArchived() {
        var active = new CertificateEntity();
        active.setStatus(CertificateEntity.Status.ACTIVE);
        active.setSubject("CN=current");

        var archived = new CertificateEntity();
        archived.setStatus(CertificateEntity.Status.ARCHIVED);
        archived.setSubject("CN=previous");
        // Set notAfter in the future so it's restorable
        archived.setNotAfter(Instant.now().plusSeconds(86400 * 365));

        when(certRepository.findByStatus(CertificateEntity.Status.ARCHIVED))
            .thenReturn(Optional.of(archived));
        when(certRepository.findByStatus(CertificateEntity.Status.ACTIVE))
            .thenReturn(Optional.of(active));

        service.restore();

        verify(certManager).restore();
        assertThat(archived.getStatus()).isEqualTo(CertificateEntity.Status.ACTIVE);
        assertThat(active.getStatus()).isEqualTo(CertificateEntity.Status.ARCHIVED);
    }

    @Test
    void restore_failsWhenArchivedExpired() {
        var archived = new CertificateEntity();
        archived.setStatus(CertificateEntity.Status.ARCHIVED);
        archived.setNotAfter(Instant.now().minusSeconds(3600)); // expired

        when(certRepository.findByStatus(CertificateEntity.Status.ARCHIVED))
            .thenReturn(Optional.of(archived));

        assertThatThrownBy(() -> service.restore())
            .isInstanceOf(IllegalStateException.class)
            .hasMessageContaining("expired");
    }

    @Test
    void discardStaged_delegatesAndDeletesEntity() {
        var staged = new CertificateEntity();
        when(certRepository.findByStatus(CertificateEntity.Status.STAGED))
            .thenReturn(Optional.of(staged));

        service.discardStaged();

        verify(certManager).discardStaged();
        verify(certRepository).delete(staged);
    }

    @Test
    void seedFromFilesystem_skipsWhenActiveExists() {
        when(certRepository.findByStatus(CertificateEntity.Status.ACTIVE))
            .thenReturn(Optional.of(new CertificateEntity()));

        service.seedFromFilesystem();

        verify(certManager, never()).getActive();
    }

    @Test
    void seedFromFilesystem_seedsFromManager() {
        var info = new CertificateInfo("CN=bootstrap", "CN=bootstrap", Instant.now(),
            Instant.now().plusSeconds(86400), false, true, "AA:BB");
        when(certRepository.findByStatus(CertificateEntity.Status.ACTIVE))
            .thenReturn(Optional.empty());
        when(certManager.getActive()).thenReturn(info);
        when(certRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        service.seedFromFilesystem();

        var captor = ArgumentCaptor.forClass(CertificateEntity.class);
        verify(certRepository).save(captor.capture());
        assertThat(captor.getValue().getSubject()).isEqualTo("CN=bootstrap");
        assertThat(captor.getValue().getStatus()).isEqualTo(CertificateEntity.Status.ACTIVE);
    }

    @Test
    void countStaleTenants_returnsZeroWhenNoActiveCert() {
        when(certRepository.findByStatus(CertificateEntity.Status.ACTIVE))
            .thenReturn(Optional.empty());

        assertThat(service.countStaleTenants()).isZero();
    }
}
