package org.raddatz.familienarchiv.geschichte;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.raddatz.familienarchiv.PostgresContainerConfig;
import org.raddatz.familienarchiv.geschichte.journeyitem.JourneyItem;
import org.raddatz.familienarchiv.user.AppUser;
import org.raddatz.familienarchiv.user.AppUserRepository;
import org.raddatz.familienarchiv.user.UserGroup;
import org.raddatz.familienarchiv.user.UserGroupRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.JdkClientHttpRequestFactory;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestTemplate;
import software.amazon.awssdk.services.s3.S3Client;

import java.io.IOException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;

/**
 * Verifies Geschichte HTTP behaviour end-to-end at the real servlet layer.
 *
 * <p>No {@code @Transactional} at class level — that would keep a session open and
 * mask LazyInitializationException caused by open-in-view: false. Each test seeds data
 * directly via repositories and relies on the service's own transaction boundaries.
 */
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
@Import(PostgresContainerConfig.class)
class GeschichteHttpTest {

    @LocalServerPort int port;
    @MockitoBean S3Client s3Client;

    @Autowired GeschichteRepository geschichteRepository;
    @Autowired AppUserRepository appUserRepository;
    @Autowired UserGroupRepository userGroupRepository;
    @Autowired PasswordEncoder passwordEncoder;

    private RestTemplate http;
    private String baseUrl;

    private static final String WRITER_EMAIL = "geschichten-http-writer@test.de";
    private static final String WRITER_PASSWORD = "pass!Geschichte1";

    @BeforeEach
    void setUp() {
        http = noThrowRestTemplate();
        baseUrl = "http://localhost:" + port;
        geschichteRepository.deleteAll();
        appUserRepository.findByEmail(WRITER_EMAIL).ifPresent(appUserRepository::delete);
        appUserRepository.findByEmail(BLOG_WRITER_EMAIL).ifPresent(appUserRepository::delete);
        userGroupRepository.findByName("HttpTest-BlogWriters").ifPresent(userGroupRepository::delete);
        appUserRepository.save(AppUser.builder()
                .email(WRITER_EMAIL)
                .password(passwordEncoder.encode(WRITER_PASSWORD))
                .build());
    }

    // ─── GET /api/geschichten ────────────────────────────────────────────────

    @Test
    void list_returns_200_and_empty_array_when_no_stories_exist() {
        String session = loginAsWriter();
        ResponseEntity<String> response = http.exchange(
                baseUrl + "/api/geschichten", HttpMethod.GET,
                new HttpEntity<>(sessionHeaders(session)), String.class);

        assertThat(response.getStatusCode().value()).isEqualTo(200);
        assertThat(response.getBody()).isEqualTo("[]");
    }

    @Test
    void list_returns_200_and_does_not_500_when_stories_have_journey_items() {
        // Seed a JOURNEY directly — items are LAZY; without @Transactional(readOnly=true) +
        // Hibernate.initialize in getById() this would 500. list() uses a projection so it
        // must also never touch items.
        AppUser writer = appUserRepository.findByEmail(WRITER_EMAIL).orElseThrow();
        Geschichte journey = Geschichte.builder()
                .title("Reise durch die Briefe")
                .status(GeschichteStatus.PUBLISHED)
                .type(GeschichteType.JOURNEY)
                .author(writer)
                .publishedAt(LocalDateTime.now())
                .items(new ArrayList<>())
                .persons(new HashSet<>())
                .build();
        JourneyItem item = JourneyItem.builder()
                .geschichte(journey)
                .position(1000)
                .note("Einleitung")
                .build();
        journey.getItems().add(item);
        geschichteRepository.save(journey);

        String session = loginAsWriter();
        ResponseEntity<String> response = http.exchange(
                baseUrl + "/api/geschichten", HttpMethod.GET,
                new HttpEntity<>(sessionHeaders(session)), String.class);

        assertThat(response.getStatusCode().value()).isEqualTo(200);
        assertThat(response.getBody()).contains("Reise durch die Briefe");
    }

    // ─── GET /api/geschichten/{id} ───────────────────────────────────────────

    @Test
    void getById_returns_200_with_items_and_does_not_500_open_in_view_false() {
        // This test is the canonical guard against LazyInitializationException.
        // open-in-view: false means the Hibernate session is closed when Jackson serializes.
        // GeschichteService.getById() must initialize items inside its @Transactional boundary.
        AppUser writer = appUserRepository.findByEmail(WRITER_EMAIL).orElseThrow();
        Geschichte journey = Geschichte.builder()
                .title("Familiengeschichte")
                .status(GeschichteStatus.PUBLISHED)
                .type(GeschichteType.JOURNEY)
                .author(writer)
                .publishedAt(LocalDateTime.now())
                .items(new ArrayList<>())
                .persons(new HashSet<>())
                .build();
        JourneyItem note = JourneyItem.builder()
                .geschichte(journey).position(1000).note("Prolog").build();
        JourneyItem note2 = JourneyItem.builder()
                .geschichte(journey).position(2000).note("Epilog").build();
        journey.getItems().add(note);
        journey.getItems().add(note2);
        Geschichte saved = geschichteRepository.save(journey);

        String session = loginAsWriter();
        ResponseEntity<String> response = http.exchange(
                baseUrl + "/api/geschichten/" + saved.getId(), HttpMethod.GET,
                new HttpEntity<>(sessionHeaders(session)), String.class);

        assertThat(response.getStatusCode().value()).isEqualTo(200);
        assertThat(response.getBody())
                .contains("Familiengeschichte")
                .contains("Prolog")
                .contains("Epilog");
    }

    @Test
    void getById_returns_404_for_unknown_id() {
        String session = loginAsWriter();
        ResponseEntity<String> response = http.exchange(
                baseUrl + "/api/geschichten/" + UUID.randomUUID(), HttpMethod.GET,
                new HttpEntity<>(sessionHeaders(session)), String.class);

        assertThat(response.getStatusCode().value()).isEqualTo(404);
        assertThat(response.getBody()).contains("GESCHICHTE_NOT_FOUND");
    }

    @Test
    void getById_returns_404_for_draft_when_reader_lacks_BLOG_WRITE() {
        AppUser writer = appUserRepository.findByEmail(WRITER_EMAIL).orElseThrow();
        Geschichte draft = Geschichte.builder()
                .title("Geheimer Entwurf")
                .status(GeschichteStatus.DRAFT)
                .author(writer)
                .items(new ArrayList<>())
                .persons(new HashSet<>())
                .build();
        Geschichte saved = geschichteRepository.save(draft);

        // Writer lacks explicit BLOG_WRITE permission in the app_users table,
        // so from the service's perspective they're a reader.
        String session = loginAsWriter();
        ResponseEntity<String> response = http.exchange(
                baseUrl + "/api/geschichten/" + saved.getId(), HttpMethod.GET,
                new HttpEntity<>(sessionHeaders(session)), String.class);

        assertThat(response.getStatusCode().value()).isEqualTo(404);
    }

    // ─── PATCH /api/geschichten/{id} ─────────────────────────────────────────

    @Test
    void update_returns_200_and_serializes_items_open_in_view_false() {
        // Canonical guard for the write path: PATCH must not 500 when the response
        // is serialized after the service transaction closed. The raw entity carries
        // a dead lazy items proxy at that point — the endpoint must answer with a
        // view assembled inside the transaction.
        AppUser writer = blogWriter();
        Geschichte journey = Geschichte.builder()
                .title("Reise vor dem Umbenennen")
                .status(GeschichteStatus.DRAFT)
                .type(GeschichteType.JOURNEY)
                .author(writer)
                .items(new ArrayList<>())
                .persons(new HashSet<>())
                .build();
        journey.getItems().add(JourneyItem.builder()
                .geschichte(journey).position(1000).note("Prolog").build());
        Geschichte saved = geschichteRepository.save(journey);

        String session = loginAs(BLOG_WRITER_EMAIL, BLOG_WRITER_PASSWORD);
        ResponseEntity<String> response = http.exchange(
                baseUrl + "/api/geschichten/" + saved.getId(), HttpMethod.PATCH,
                new HttpEntity<>("{\"title\":\"Reise nach dem Umbenennen\"}", csrfJsonHeaders(session)),
                String.class);

        assertThat(response.getStatusCode().value()).isEqualTo(200);
        assertThat(response.getBody())
                .contains("Reise nach dem Umbenennen")
                .contains("Prolog");
    }

    // ─── helpers ─────────────────────────────────────────────────────────────

    private static final String BLOG_WRITER_EMAIL = "geschichten-http-blogwriter@test.de";
    private static final String BLOG_WRITER_PASSWORD = "pass!Geschichte2";

    /** A user whose group actually grants BLOG_WRITE — unlike the plain writer above. */
    private AppUser blogWriter() {
        UserGroup group = userGroupRepository.save(UserGroup.builder()
                .name("HttpTest-BlogWriters")
                .permissions(new HashSet<>(Set.of("BLOG_WRITE")))
                .build());
        return appUserRepository.save(AppUser.builder()
                .email(BLOG_WRITER_EMAIL)
                .password(passwordEncoder.encode(BLOG_WRITER_PASSWORD))
                .groups(new HashSet<>(Set.of(group)))
                .build());
    }

    /** Session cookie + double-submit CSRF pair + JSON content type for write requests. */
    private HttpHeaders csrfJsonHeaders(String sessionId) {
        String xsrf = UUID.randomUUID().toString();
        HttpHeaders headers = new HttpHeaders();
        headers.set("Cookie", "fa_session=" + sessionId + "; XSRF-TOKEN=" + xsrf);
        headers.set("X-XSRF-TOKEN", xsrf);
        headers.setContentType(MediaType.APPLICATION_JSON);
        return headers;
    }

    private String loginAsWriter() {
        return loginAs(WRITER_EMAIL, WRITER_PASSWORD);
    }

    private String loginAs(String email, String password) {
        String xsrf = UUID.randomUUID().toString();
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.set("Cookie", "XSRF-TOKEN=" + xsrf);
        headers.set("X-XSRF-TOKEN", xsrf);
        String body = "{\"email\":\"" + email + "\",\"password\":\"" + password + "\"}";
        ResponseEntity<String> resp = http.postForEntity(
                baseUrl + "/api/auth/login", new HttpEntity<>(body, headers), String.class);
        return extractFaSessionCookie(resp);
    }

    private HttpHeaders sessionHeaders(String sessionId) {
        HttpHeaders headers = new HttpHeaders();
        headers.set("Cookie", "fa_session=" + sessionId);
        return headers;
    }

    private String extractFaSessionCookie(ResponseEntity<?> response) {
        List<String> setCookieHeader = response.getHeaders().get("Set-Cookie");
        if (setCookieHeader == null) return "";
        return setCookieHeader.stream()
                .filter(c -> c.startsWith("fa_session="))
                .map(c -> c.split(";")[0].substring("fa_session=".length()))
                .findFirst()
                .orElse("");
    }

    private RestTemplate noThrowRestTemplate() {
        // JDK HttpClient factory — the default HttpURLConnection factory cannot send PATCH.
        RestTemplate template = new RestTemplate(new JdkClientHttpRequestFactory());
        template.setErrorHandler(new DefaultResponseErrorHandler() {
            @Override
            public boolean hasError(ClientHttpResponse response) throws IOException {
                return false;
            }
        });
        return template;
    }
}
