package com.magistr.app.service;

import com.magistr.app.dto.AcademicCalendarGridDayDto;
import com.magistr.app.model.AcademicCalendarDay;
import com.magistr.app.repository.AcademicCalendarDayRepository;
import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.transaction.support.TransactionTemplate;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import java.time.LocalDate;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

@Testcontainers
@SpringBootTest(
        classes = AcademicCalendarGridServiceIntegrationTest.TestApplication.class,
        properties = {
                "spring.jpa.open-in-view=false",
                "management.endpoint.health.validate-group-membership=false"
        }
)
class AcademicCalendarGridServiceIntegrationTest {

    @Container
    static final PostgreSQLContainer<?> POSTGRES =
            new PostgreSQLContainer<>(com.magistr.app.testing.TestContainerImages.POSTGRES);

    @DynamicPropertySource
    static void databaseProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", POSTGRES::getJdbcUrl);
        registry.add("spring.datasource.username", POSTGRES::getUsername);
        registry.add("spring.datasource.password", POSTGRES::getPassword);
    }

    @Autowired
    private AcademicCalendarGridService service;

    @Autowired
    private ScheduleGeneratorService scheduleGeneratorService;

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Autowired
    private TransactionTemplate transactionTemplate;

    @Test
    void invalidSecondRowKeepsOldGridAndSuccessfulReplacementIsAtomic() {
        assertThat(AopUtils.isAopProxy(service)).isTrue();

        long calendarId = queryLong("""
                SELECT calendar.id
                FROM academic_calendars calendar
                JOIN academic_years year ON year.id = calendar.academic_year_id
                WHERE year.title = '2025-2026'
                ORDER BY calendar.id
                LIMIT 1
                """);
        long theoryActivityId = queryLong(
                "SELECT id FROM academic_calendar_activity_types WHERE code = 'Т'"
        );
        long holidayActivityId = queryLong(
                "SELECT id FROM academic_calendar_activity_types WHERE code = 'К'"
        );
        LocalDate yearStart = jdbcTemplate.queryForObject("""
                SELECT year.start_date
                FROM academic_calendars calendar
                JOIN academic_years year ON year.id = calendar.academic_year_id
                WHERE calendar.id = ?
                """, LocalDate.class, calendarId);
        assertThat(yearStart).isEqualTo(LocalDate.of(2025, 9, 1));

        GridFingerprint before = fingerprint(calendarId);
        assertThat(before.rowCount()).isPositive();
        AcademicCalendarGridDayDto firstValid = row(
                calendarId, 1, yearStart, 1, 1, holidayActivityId, "К"
        );
        AcademicCalendarGridDayDto secondInvalid = row(
                calendarId, 1, yearStart.plusDays(1), 1, 7, theoryActivityId, "Т"
        );

        assertThatThrownBy(() -> service.replaceGrid(
                calendarId,
                List.of(firstValid, secondInvalid)
        ))
                .isInstanceOf(IllegalArgumentException.class)
                .hasMessage("День недели не соответствует дате");

        assertThat(fingerprint(calendarId)).isEqualTo(before);
        assertThat(queryLong("""
                SELECT count(*)
                FROM academic_calendar_days day
                WHERE day.calendar_id = ?
                  AND day.course_number = 1
                  AND day.date = ?
                  AND day.activity_type_id = ?
                """, calendarId, yearStart, holidayActivityId)).isZero();
        verify(scheduleGeneratorService, never()).clearCache();

        AcademicCalendarGridDayDto secondValid = row(
                calendarId, 1, yearStart.plusDays(1), 1, 2, theoryActivityId, "Т"
        );
        transactionTemplate.executeWithoutResult(status -> {
            service.replaceGrid(calendarId, List.of(firstValid, secondValid));
            verify(scheduleGeneratorService, never()).clearCache();
        });

        GridFingerprint after = fingerprint(calendarId);
        assertThat(after.rowCount()).isEqualTo(2L);
        assertThat(after).isNotEqualTo(before);
        assertThat(queryLong("""
                SELECT count(*)
                FROM academic_calendar_days day
                WHERE day.calendar_id = ?
                  AND (
                    (day.course_number = 1 AND day.date = ? AND day.activity_type_id = ?)
                    OR
                    (day.course_number = 1 AND day.date = ? AND day.activity_type_id = ?)
                  )
                """, calendarId, yearStart, holidayActivityId,
                yearStart.plusDays(1), theoryActivityId)).isEqualTo(2L);
        verify(scheduleGeneratorService, times(1)).clearCache();
    }

    private GridFingerprint fingerprint(long calendarId) {
        GridFingerprint fingerprint = jdbcTemplate.queryForObject("""
                SELECT
                    count(*) AS row_count,
                    md5(string_agg(
                        concat_ws('|',
                            day.course_number::text,
                            day.date::text,
                            day.week_number::text,
                            day.day_of_week::text,
                            day.activity_type_id::text
                        ),
                        E'\\n' ORDER BY day.course_number, day.date, day.id
                    )) AS content_hash
                FROM academic_calendar_days day
                WHERE day.calendar_id = ?
                """, (result, rowNumber) -> new GridFingerprint(
                result.getLong("row_count"),
                result.getString("content_hash")
        ), calendarId);
        assertThat(fingerprint).isNotNull();
        return fingerprint;
    }

    private long queryLong(String sql, Object... arguments) {
        Long value = jdbcTemplate.queryForObject(sql, Long.class, arguments);
        assertThat(value).isNotNull();
        return value;
    }

    private AcademicCalendarGridDayDto row(long calendarId,
                                           int course,
                                           LocalDate date,
                                           int week,
                                           int dayOfWeek,
                                           long activityTypeId,
                                           String activityCode) {
        return new AcademicCalendarGridDayDto(
                null,
                calendarId,
                course,
                date,
                week,
                dayOfWeek,
                activityTypeId,
                activityCode,
                null,
                null
        );
    }

    @SpringBootConfiguration
    @EnableAutoConfiguration
    @EntityScan(basePackageClasses = AcademicCalendarDay.class)
    @EnableJpaRepositories(basePackageClasses = AcademicCalendarDayRepository.class)
    @Import({AcademicCalendarGridService.class, TestDependencies.class})
    static class TestApplication {
    }

    @TestConfiguration(proxyBeanMethods = false)
    static class TestDependencies {

        @Bean
        ScheduleGeneratorService scheduleGeneratorService() {
            return mock(ScheduleGeneratorService.class);
        }
    }

    private record GridFingerprint(long rowCount, String contentHash) {
    }
}
