package net.siegeln.cameleer.saas.vendor;

import net.siegeln.cameleer.saas.audit.AuditService;
import net.siegeln.cameleer.saas.identity.LogtoConfig;
import net.siegeln.cameleer.saas.identity.LogtoManagementClient;
import net.siegeln.cameleer.saas.identity.ServerApiClient;
import net.siegeln.cameleer.saas.provisioning.ProvisioningProperties;
import net.siegeln.cameleer.saas.license.LicenseEntity;
import net.siegeln.cameleer.saas.license.LicenseService;
import net.siegeln.cameleer.saas.provisioning.ProvisionResult;
import net.siegeln.cameleer.saas.provisioning.TenantProvisioner;
import net.siegeln.cameleer.saas.tenant.TenantEntity;
import net.siegeln.cameleer.saas.tenant.TenantRepository;
import net.siegeln.cameleer.saas.tenant.TenantService;
import net.siegeln.cameleer.saas.tenant.TenantStatus;
import net.siegeln.cameleer.saas.tenant.Tier;
import net.siegeln.cameleer.saas.tenant.dto.CreateTenantRequest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.time.Duration;
import java.time.Instant;
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.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class VendorTenantServiceTest {

    @Mock
    private TenantService tenantService;

    @Mock
    private TenantRepository tenantRepository;

    @Mock
    private LicenseService licenseService;

    @Mock
    private TenantProvisioner tenantProvisioner;

    @Mock
    private ServerApiClient serverApiClient;

    @Mock
    private LogtoManagementClient logtoClient;

    @Mock
    private LogtoConfig logtoConfig;

    @Mock
    private AuditService auditService;

    private VendorTenantService vendorTenantService;

    @BeforeEach
    void setUp() {
        var provisioningProps = new ProvisioningProperties(
            "img", "uiimg", "net", "traefik", "localhost", "https",
            "jdbc:postgresql://pg:5432/db", "https://localhost/oidc",
            "http://logto:3001/oidc/jwks", "https://localhost");
        vendorTenantService = new VendorTenantService(
                tenantService, tenantRepository, licenseService,
                tenantProvisioner, serverApiClient, logtoClient, logtoConfig,
                auditService, provisioningProps);
    }

    // --- Helpers ---

    private TenantEntity tenantWithId(String name, String slug, Tier tier) throws Exception {
        var tenant = new TenantEntity();
        tenant.setName(name);
        tenant.setSlug(slug);
        tenant.setTier(tier);
        tenant.setStatus(TenantStatus.PROVISIONING);
        var f = TenantEntity.class.getDeclaredField("id");
        f.setAccessible(true);
        f.set(tenant, UUID.randomUUID());
        return tenant;
    }

    private LicenseEntity licenseWithId(UUID tenantId) throws Exception {
        var license = new LicenseEntity();
        license.setTenantId(tenantId);
        license.setTier("LOW");
        license.setToken("test-token-" + UUID.randomUUID());
        license.setIssuedAt(Instant.now());
        license.setExpiresAt(Instant.now().plus(Duration.ofDays(365)));
        var f = LicenseEntity.class.getDeclaredField("id");
        f.setAccessible(true);
        f.set(license, UUID.randomUUID());
        return license;
    }

    // --- Tests ---

    @Test
    void createAndProvision_createsTenantAndLicense() throws Exception {
        var request = new CreateTenantRequest("Acme Corp", "acme-corp", "LOW", null, null);
        var actorId = UUID.randomUUID();
        var tenant = tenantWithId("Acme Corp", "acme-corp", Tier.LOW);
        var license = licenseWithId(tenant.getId());

        when(tenantService.create(request, actorId)).thenReturn(tenant);
        when(licenseService.generateLicense(eq(tenant), any(Duration.class), eq(actorId))).thenReturn(license);
        when(tenantProvisioner.isAvailable()).thenReturn(false);

        var result = vendorTenantService.createAndProvision(request, actorId);

        verify(tenantService).create(request, actorId);
        verify(licenseService).generateLicense(eq(tenant), any(Duration.class), eq(actorId));
        assertThat(result).isNotNull();
        assertThat(result.getName()).isEqualTo("Acme Corp");
        assertThat(result.getSlug()).isEqualTo("acme-corp");
    }

    @Test
    void createAndProvision_setsActiveWhenProvisionerSucceeds() throws Exception {
        var request = new CreateTenantRequest("Acme Corp", "acme-corp", "LOW", null, null);
        var actorId = UUID.randomUUID();
        var tenant = tenantWithId("Acme Corp", "acme-corp", Tier.LOW);
        var license = licenseWithId(tenant.getId());

        when(tenantService.create(request, actorId)).thenReturn(tenant);
        when(licenseService.generateLicense(eq(tenant), any(Duration.class), eq(actorId))).thenReturn(license);
        when(tenantProvisioner.isAvailable()).thenReturn(true);
        when(tenantProvisioner.provision(any())).thenReturn(ProvisionResult.ok("http://server:8080"));
        when(tenantRepository.save(any(TenantEntity.class))).thenAnswer(inv -> inv.getArgument(0));

        var result = vendorTenantService.createAndProvision(request, actorId);

        assertThat(result.getStatus()).isEqualTo(TenantStatus.ACTIVE);
        assertThat(result.getServerEndpoint()).isEqualTo("http://server:8080");
        assertThat(result.getProvisionError()).isNull();
        verify(tenantRepository).save(tenant);
    }

    @Test
    void createAndProvision_setsProvisionErrorOnFailure() throws Exception {
        var request = new CreateTenantRequest("Acme Corp", "acme-corp", "LOW", null, null);
        var actorId = UUID.randomUUID();
        var tenant = tenantWithId("Acme Corp", "acme-corp", Tier.LOW);
        var license = licenseWithId(tenant.getId());

        when(tenantService.create(request, actorId)).thenReturn(tenant);
        when(licenseService.generateLicense(eq(tenant), any(Duration.class), eq(actorId))).thenReturn(license);
        when(tenantProvisioner.isAvailable()).thenReturn(true);
        when(tenantProvisioner.provision(any())).thenReturn(ProvisionResult.fail("Docker failure"));
        when(tenantRepository.save(any(TenantEntity.class))).thenAnswer(inv -> inv.getArgument(0));

        var result = vendorTenantService.createAndProvision(request, actorId);

        assertThat(result.getProvisionError()).isEqualTo("Docker failure");
        assertThat(result.getStatus()).isEqualTo(TenantStatus.PROVISIONING);
        verify(tenantRepository).save(tenant);
    }

    @Test
    void createAndProvision_worksWithoutProvisioner() throws Exception {
        var request = new CreateTenantRequest("Acme Corp", "acme-corp", "LOW", null, null);
        var actorId = UUID.randomUUID();
        var tenant = tenantWithId("Acme Corp", "acme-corp", Tier.LOW);
        var license = licenseWithId(tenant.getId());

        when(tenantService.create(request, actorId)).thenReturn(tenant);
        when(licenseService.generateLicense(eq(tenant), any(Duration.class), eq(actorId))).thenReturn(license);
        when(tenantProvisioner.isAvailable()).thenReturn(false);

        var result = vendorTenantService.createAndProvision(request, actorId);

        verify(tenantProvisioner, never()).provision(any());
        verify(tenantRepository, never()).save(any());
        assertThat(result).isSameAs(tenant);
        // Tenant was created by tenantService.create; no provisioner means status unchanged (PROVISIONING from factory)
        assertThat(result.getStatus()).isEqualTo(TenantStatus.PROVISIONING);
    }

    @Test
    void suspend_stopsContainersAndSetsStatus() throws Exception {
        var tenantId = UUID.randomUUID();
        var actorId = UUID.randomUUID();
        var tenant = tenantWithId("Acme Corp", "acme-corp", Tier.LOW);
        var suspended = tenantWithId("Acme Corp", "acme-corp", Tier.LOW);
        suspended.setStatus(TenantStatus.SUSPENDED);

        when(tenantService.getById(tenantId)).thenReturn(Optional.of(tenant));
        when(tenantProvisioner.isAvailable()).thenReturn(true);
        when(tenantService.suspend(tenantId, actorId)).thenReturn(suspended);

        var result = vendorTenantService.suspend(tenantId, actorId);

        verify(tenantProvisioner).stop("acme-corp");
        verify(tenantService).suspend(tenantId, actorId);
        assertThat(result.getStatus()).isEqualTo(TenantStatus.SUSPENDED);
    }

    @Test
    void suspend_skipsStopWhenProvisionerUnavailable() throws Exception {
        var tenantId = UUID.randomUUID();
        var actorId = UUID.randomUUID();
        var tenant = tenantWithId("Acme Corp", "acme-corp", Tier.LOW);
        var suspended = tenantWithId("Acme Corp", "acme-corp", Tier.LOW);
        suspended.setStatus(TenantStatus.SUSPENDED);

        when(tenantService.getById(tenantId)).thenReturn(Optional.of(tenant));
        when(tenantProvisioner.isAvailable()).thenReturn(false);
        when(tenantService.suspend(tenantId, actorId)).thenReturn(suspended);

        vendorTenantService.suspend(tenantId, actorId);

        verify(tenantProvisioner, never()).stop(any());
        verify(tenantService).suspend(tenantId, actorId);
    }

    @Test
    void delete_removesContainersRevokesLicenseDeletesOrg() throws Exception {
        var tenantId = UUID.randomUUID();
        var actorId = UUID.randomUUID();
        var tenant = tenantWithId("Acme Corp", "acme-corp", Tier.LOW);
        tenant.setLogtoOrgId("org-123");

        when(tenantService.getById(tenantId)).thenReturn(Optional.of(tenant));
        when(tenantProvisioner.isAvailable()).thenReturn(true);
        when(logtoClient.isAvailable()).thenReturn(true);
        when(tenantRepository.save(any(TenantEntity.class))).thenAnswer(inv -> inv.getArgument(0));

        vendorTenantService.delete(tenantId, actorId);

        verify(tenantProvisioner).remove("acme-corp");
        verify(licenseService).revokeLicense(tenantId, actorId);
        verify(logtoClient).deleteOrganization("org-123");
        verify(tenantRepository).save(tenant);
        assertThat(tenant.getStatus()).isEqualTo(TenantStatus.DELETED);
    }

    @Test
    void renewLicense_revokesOldAndGeneratesNew() throws Exception {
        var tenantId = UUID.randomUUID();
        var actorId = UUID.randomUUID();
        var tenant = tenantWithId("Acme Corp", "acme-corp", Tier.LOW);
        tenant.setServerEndpoint("http://server:8080");
        var newLicense = licenseWithId(tenantId);

        when(tenantService.getById(tenantId)).thenReturn(Optional.of(tenant));
        when(licenseService.generateLicense(eq(tenant), any(Duration.class), eq(actorId))).thenReturn(newLicense);

        var result = vendorTenantService.renewLicense(tenantId, actorId);

        verify(licenseService).revokeLicense(tenantId, actorId);
        verify(licenseService).generateLicense(eq(tenant), any(Duration.class), eq(actorId));
        verify(serverApiClient).pushLicense("http://server:8080", newLicense.getToken());
        assertThat(result).isSameAs(newLicense);
    }
}
