package demo;

import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;

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

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.FactorGrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;

/** Variant 2: only /admin/** needs the second factor, and it must be fresh. */
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("stepup")
class MfaStepUpTest extends MfaTestSupport {

    @Autowired MockMvc mvc;
    @Autowired MfaTokenCatcher catcher;

    @BeforeAll
    static void clean() {
        new java.io.File("cap/11-stepup-flow.txt").delete();
        new java.io.File("cap/12-stepup-freshness.txt").delete();
    }

    private static String where(MockHttpServletResponse r) {
        return r.getStatus() + (r.getRedirectedUrl() != null ? " -> " + r.getRedirectedUrl() : "");
    }

    @Test
    void realStepUpFlow() throws Exception {
        String f = "11-stepup-flow.txt";
        var login = mvc.perform(formLogin().user("alice").password("pw")).andReturn();
        MockHttpSession session = (MockHttpSession) login.getRequest().getSession();
        say(f, "1  after password login, GET /api/profile : "
                + mvc.perform(get("/api/profile").session(session)).andReturn().getResponse().getStatus());
        var admin = mvc.perform(get("/admin/panel").session(session).accept("text/html")).andReturn().getResponse();
        say(f, "2  GET /admin/panel (password only)       : " + where(admin));

        mvc.perform(post("/ott/generate").param("username", "alice").session(session).with(csrf()));
        var ott = mvc.perform(post("/login/ott").param("token", catcher.last()).session(session).with(csrf())).andReturn();
        MockHttpSession after = (MockHttpSession) ott.getRequest().getSession();
        say(f, "3  token submitted                        : " + where(ott.getResponse()));
        say(f, "   authorities after token login          : " + authorities(after));
        var ok = mvc.perform(get("/admin/panel").session(after).accept("text/html")).andReturn().getResponse();
        say(f, "4  GET /admin/panel (password + token)    : " + where(ok) + " " + ok.getContentAsString());
        assertThat(admin.getStatus()).isEqualTo(302);
        assertThat(ok.getStatus()).isEqualTo(200);
    }

    private static UsernamePasswordAuthenticationToken user(Instant ottIssuedAt) {
        List<GrantedAuthority> a = new java.util.ArrayList<>(List.of(
                new SimpleGrantedAuthority("ROLE_USER"), new SimpleGrantedAuthority("ROLE_ADMIN"),
                FactorGrantedAuthority.withAuthority(FactorGrantedAuthority.PASSWORD_AUTHORITY).issuedAt(Instant.now()).build()));
        if (ottIssuedAt != null) {
            a.add(FactorGrantedAuthority.withAuthority(FactorGrantedAuthority.OTT_AUTHORITY).issuedAt(ottIssuedAt).build());
        }
        return UsernamePasswordAuthenticationToken.authenticated("alice", null, a);
    }

    @Test
    void factorMustBeFresh() throws Exception {
        String f = "12-stepup-freshness.txt";
        var fresh = mvc.perform(get("/admin/panel").with(authentication(user(Instant.now().minusSeconds(60)))))
                .andReturn().getResponse();
        say(f, "token issued 1 minute ago   : " + fresh.getStatus());
        var stale = mvc.perform(get("/admin/panel").accept("text/html")
                .with(authentication(user(Instant.now().minusSeconds(600))))).andReturn().getResponse();
        say(f, "token issued 10 minutes ago : " + where(stale));
        var none = mvc.perform(get("/admin/panel").accept("text/html").with(authentication(user(null)))).andReturn().getResponse();
        say(f, "no token at all             : " + where(none));
        assertThat(fresh.getStatus()).isEqualTo(200);
        assertThat(stale.getStatus()).isEqualTo(302);
    }
}
