use linkerd_policy_controller_k8s_api::{
    self as k8s, policy::server_authorization::Client as ClientAuthz, ResourceExt,
};
use linkerd_policy_test::{
    await_condition, create, create_ready_pod, curl, endpoints_ready, web, with_temp_ns,
    LinkerdInject,
};

#[tokio::test(flavor = "current_thread")]
async fn meshtls() {
    with_temp_ns(|client, ns| async move {
        let srv = create(&client, web::server(&ns, None)).await;

        create(
            &client,
            server_authz(
                &ns,
                "web",
                &srv,
                ClientAuthz {
                    mesh_tls: Some(k8s::policy::server_authorization::MeshTls {
                        identities: Some(vec!["*".to_string()]),
                        ..Default::default()
                    }),
                    ..Default::default()
                },
            ),
        )
        .await;

        // Create the web pod and wait for it to be ready.
        tokio::join!(
            create(&client, web::service(&ns)),
            create_ready_pod(&client, web::pod(&ns))
        );

        await_condition(&client, &ns, "web", endpoints_ready).await;

        let curl = curl::Runner::init(&client, &ns).await;
        let (injected, uninjected) = tokio::join!(
            curl.run("curl-injected", "http://web", LinkerdInject::Enabled),
            curl.run("curl-uninjected", "http://web", LinkerdInject::Disabled),
        );
        let (injected_status, uninjected_status) =
            tokio::join!(injected.exit_code(), uninjected.exit_code());
        assert_eq!(injected_status, 0, "injected curl must succeed");
        assert_eq!(uninjected_status, 22, "uninjected curl must fail");
    })
    .await;
}

#[tokio::test(flavor = "current_thread")]
async fn network() {
    with_temp_ns(|client, ns| async move {
        let curl = curl::Runner::init(&client, &ns).await;

        // Create a lock that prevents `curl` pods from running. This allows us
        // to create a curl pod so that its IP can be obtained to be used in an
        // authorization policy.
        curl.create_lock().await;

        // Create a curl pod and wait for it to get an IP.
        let blessed = curl
            .run("curl-blessed", "http://web", LinkerdInject::Disabled)
            .await;
        let blessed_ip = blessed.ip().await;
        tracing::debug!(curl.blessed.ip = %blessed_ip);

        // Once we know the IP of the (blocked) pod, create an web
        // authorization policy that permits connections from this pod.
        let srv = create(&client, web::server(&ns, None)).await;
        create(
            &client,
            server_authz(
                &ns,
                "web",
                &srv,
                ClientAuthz {
                    networks: Some(vec![k8s::policy::Network {
                        cidr: blessed_ip.into(),
                        except: None,
                    }]),
                    unauthenticated: true,
                    ..Default::default()
                },
            ),
        )
        .await;

        // Start web with the policy.
        tokio::join!(
            create(&client, web::service(&ns)),
            create_ready_pod(&client, web::pod(&ns))
        );

        await_condition(&client, &ns, "web", endpoints_ready).await;

        // Once the web pod is ready, delete the `curl-lock` configmap to
        // unblock curl from running.
        curl.delete_lock().await;
        tracing::info!("unblocked curl");

        // The blessed pod should be able to connect to the web pod.
        let status = blessed.exit_code().await;
        assert_eq!(status, 0, "blessed curl must succeed");

        // Create another curl pod that is not included in the authorization. It
        // should fail to connect to the web pod.
        let status = curl
            .run("curl-cursed", "http://web", LinkerdInject::Disabled)
            .await
            .exit_code()
            .await;
        assert_eq!(status, 22, "cursed curl must fail");
    })
    .await;
}

#[tokio::test(flavor = "current_thread")]
async fn both() {
    with_temp_ns(|client, ns| async move {
        let curl = curl::Runner::init(&client, &ns).await;

        // Create a lock that prevents `curl` pods from running. This allows us
        // to create a curl pod so that its IP can be obtained to be used in an
        // authorization policy.
        curl.create_lock().await;

        let (blessed_injected, blessed_uninjected) = tokio::join!(
            curl.run(
                "curl-blessed-injected",
                "http://web",
                LinkerdInject::Enabled,
            ),
            curl.run(
                "curl-blessed-uninjected",
                "http://web",
                LinkerdInject::Disabled,
            )
        );
        let (blessed_injected_ip, blessed_uninjected_ip) =
            tokio::join!(blessed_injected.ip(), blessed_uninjected.ip(),);
        tracing::debug!(curl.blessed.injected.ip = ?blessed_injected_ip);
        tracing::debug!(curl.blessed.uninjected.ip = ?blessed_uninjected_ip);

        // Once we know the IP of the (blocked) pod, create an web
        // authorization policy that permits connections from this pod.
        let srv = create(&client, web::server(&ns, None)).await;
        create(
            &client,
            server_authz(
                &ns,
                "web",
                &srv,
                ClientAuthz {
                    networks: Some(vec![
                        k8s::policy::Network {
                            cidr: blessed_injected_ip.into(),
                            except: None,
                        },
                        k8s::policy::Network {
                            cidr: blessed_uninjected_ip.into(),
                            except: None,
                        },
                    ]),
                    mesh_tls: Some(k8s::policy::server_authorization::MeshTls {
                        identities: Some(vec!["*".to_string()]),
                        ..Default::default()
                    }),
                    ..Default::default()
                },
            ),
        )
        .await;

        // Start web with the policy.
        tokio::join!(
            create(&client, web::service(&ns)),
            create_ready_pod(&client, web::pod(&ns))
        );

        await_condition(&client, &ns, "web", endpoints_ready).await;

        // Once the web pod is ready, delete the `curl-lock` configmap to
        // unblock curl from running.
        curl.delete_lock().await;
        tracing::info!("unblocked curl");

        let (blessed_injected_status, blessed_uninjected_status) =
            tokio::join!(blessed_injected.exit_code(), blessed_uninjected.exit_code());
        // The blessed and injected pod should be able to connect to the web pod.
        assert_eq!(
            blessed_injected_status, 0,
            "blessed injected curl must succeed"
        );
        // The blessed and uninjected pod should NOT be able to connect to the web pod.
        assert_eq!(
            blessed_uninjected_status, 22,
            "blessed uninjected curl must fail"
        );

        let (cursed_injected, cursed_uninjected) = tokio::join!(
            curl.run("curl-cursed-injected", "http://web", LinkerdInject::Enabled,),
            curl.run(
                "curl-cursed-uninjected",
                "http://web",
                LinkerdInject::Disabled,
            )
        );
        let (cursed_injected_status, cursed_uninjected_status) =
            tokio::join!(cursed_injected.exit_code(), cursed_uninjected.exit_code(),);
        assert_eq!(cursed_injected_status, 22, "cursed injected curl must fail");
        assert_eq!(
            cursed_uninjected_status, 22,
            "cursed uninjected curl must fail"
        );
    })
    .await;
}

#[tokio::test(flavor = "current_thread")]
async fn either() {
    with_temp_ns(|client, ns| async move {
        let curl = curl::Runner::init(&client, &ns).await;

        tracing::info!("Blocking curl");
        curl.create_lock().await;

        let (blessed_injected, blessed_uninjected) = tokio::join!(
            curl.run(
                "curl-blessed-injected",
                "http://web",
                LinkerdInject::Enabled,
            ),
            curl.run(
                "curl-blessed-uninjected",
                "http://web",
                LinkerdInject::Disabled,
            )
        );
        let (blessed_injected_ip, blessed_uninjected_ip) =
            tokio::join!(blessed_injected.ip(), blessed_uninjected.ip());
        tracing::debug!(curl.blessed.injected.ip = ?blessed_injected_ip);
        tracing::debug!(curl.blessed.uninjected.ip = ?blessed_uninjected_ip);

        // Once we know the IP of the (blocked) pod, create an web
        // authorization policy that permits connections from this pod.
        let srv = create(&client, web::server(&ns, None)).await;
        tokio::join!(
            create(
                &client,
                server_authz(
                    &ns,
                    "web-from-ip",
                    &srv,
                    ClientAuthz {
                        unauthenticated: true,
                        networks: Some(vec![
                            k8s::policy::Network {
                                cidr: blessed_injected_ip.into(),
                                except: None,
                            },
                            k8s::policy::Network {
                                cidr: blessed_uninjected_ip.into(),
                                except: None,
                            },
                        ]),
                        ..Default::default()
                    },
                )
            ),
            create(
                &client,
                server_authz(
                    &ns,
                    "web-from-id",
                    &srv,
                    ClientAuthz {
                        mesh_tls: Some(k8s::policy::server_authorization::MeshTls {
                            identities: Some(vec!["*".to_string()]),
                            ..Default::default()
                        }),
                        ..Default::default()
                    },
                )
            ),
        );

        // Start web with the policy.
        tokio::join!(
            create(&client, web::service(&ns)),
            create_ready_pod(&client, web::pod(&ns)),
        );

        await_condition(&client, &ns, "web", endpoints_ready).await;

        // Once the web pod is ready, delete the `curl-lock` configmap to
        // unblock curl from running.
        curl.delete_lock().await;
        tracing::info!("unblocked curl");

        let (blessed_injected_status, blessed_uninjected_status) =
            tokio::join!(blessed_injected.exit_code(), blessed_uninjected.exit_code());
        assert_eq!(
            blessed_injected_status, 0,
            "blessed injected curl must succeed"
        );
        assert_eq!(
            blessed_uninjected_status, 0,
            "blessed uninjected curl must succeed"
        );

        let (cursed_injected, cursed_uninjected) = tokio::join!(
            curl.run("curl-cursed-injected", "http://web", LinkerdInject::Enabled,),
            curl.run(
                "curl-cursed-uninjected",
                "http://web",
                LinkerdInject::Disabled,
            ),
        );
        let (cursed_injected_status, cursed_uninjected_status) =
            tokio::join!(cursed_injected.exit_code(), cursed_uninjected.exit_code());
        assert_eq!(
            cursed_injected_status, 0,
            "cursed injected curl must succeed"
        );
        assert_eq!(
            cursed_uninjected_status, 22,
            "cursed uninjected curl must fail"
        );
    })
    .await;
}

// === helpers ===

fn server_authz(
    ns: &str,
    name: &str,
    target: &k8s::policy::Server,
    client: ClientAuthz,
) -> k8s::policy::ServerAuthorization {
    k8s::policy::ServerAuthorization {
        metadata: k8s::ObjectMeta {
            namespace: Some(ns.to_string()),
            name: Some(name.to_string()),
            ..Default::default()
        },
        spec: k8s::policy::ServerAuthorizationSpec {
            server: k8s::policy::server_authorization::Server {
                name: Some(target.name_unchecked()),
                selector: None,
            },
            client,
        },
    }
}
