//! End-to-end integration tests for pijul-srv-lite.

mod support;

use anyhow::Result;
use assert_fs::prelude::*;
use reqwest::blocking::Client;
use reqwest::blocking::get;
use serde_json::Value;

use crate::support::RepoFixture;
use crate::support::TestServer;
use crate::support::{PijulEnv, pijul_env::run_pijul};

#[test]
fn serves_response_for_library_initialized_repo() -> Result<()> {
    let repo = RepoFixture::new().expect("failed to create repo fixture");
    let server = TestServer::start(repo).expect("failed to start server");

    let resp = dbg!(get(&format!("{}/.pijul", server.base_url()))?);

    assert_eq!(resp.status(), 200);
    assert!(
        resp.headers()
            .get("Content-Type")
            .is_some_and(|t| t == "application/json")
    );

    let body: Value = serde_json::from_str(&resp.text()?)?;
    let channels = body
        .get("channels")
        .and_then(|c| c.as_array())
        .expect("expected channels array");
    assert!(channels.iter().any(|c| c == "main"));
    assert!(body.get("head").is_some());

    Ok(())
}

#[test]
fn test_all_endpoints() -> Result<()> {
    let repo = RepoFixture::new()?;
    repo.write_file(".pijul/changes/ab/cdef123.change", "change data")?;
    repo.write_file(".pijul/changes/ab/cdef123.tag", "tag data")?;
    let server = TestServer::start(repo)?;
    let url = format!("{}/.pijul", server.base_url());

    let client = &Client::builder().user_agent("pijul-1.0.0-beta9").build()?;

    // info (?log alias)
    {
        let resp = client.get(&url).query(&[("log", "")]).send()?;
        assert_eq!(resp.status(), 200);
        assert!(
            resp.headers()
                .get("Content-Type")
                .is_some_and(|t| t == "application/json")
        );
    }

    // identities
    {
        let resp = client.get(&url).query(&[("identities", "")]).send()?;
        assert_eq!(resp.status(), 200);
        assert!(
            resp.headers()
                .get("Content-Type")
                .is_some_and(|t| t == "application/json")
        );
        let body = resp.text()?;
        assert_eq!(body, r#"{"id":[],"rev":0}"#);
    }

    // channel id
    {
        let resp = client
            .get(&url)
            .query(&[("channel", "main"), ("id", "")])
            .send()?;
        assert_eq!(resp.status(), 200);
        assert!(
            resp.headers()
                .get("Content-Type")
                .is_some_and(|t| t == "application/octet-stream")
        );
        let bytes = resp.bytes()?;
        assert_eq!(bytes.len(), 16);
    }

    // channel state summary (JSON)
    {
        let resp = client.get(&url).query(&[("channel", "main")]).send()?;
        assert_eq!(resp.status(), 200);
        assert!(
            resp.headers()
                .get("Content-Type")
                .is_some_and(|t| t == "application/json")
        );
        let body: Value = serde_json::from_str(&resp.text()?)?;
        assert!(body.get("position").and_then(|v| v.as_u64()).is_some());
        assert!(body.get("merkle").and_then(|v| v.as_str()).is_some());
        assert!(body.get("tag_merkle").and_then(|v| v.as_str()).is_some());
    }

    // state at position
    {
        let resp = client
            .get(&url)
            .query(&[("state", "0"), ("channel", "main")])
            .send()?;
        assert_eq!(resp.status(), 200);
        assert!(
            resp.headers()
                .get("Content-Type")
                .is_some_and(|t| t == "text/plain")
        );
        let body = resp.text()?;
        let parts: Vec<&str> = body.split_whitespace().collect();
        assert_eq!(parts.len(), 3);
        assert!(parts[0].parse::<u64>().is_ok());
    }

    // changelist from position
    {
        let resp = client
            .get(&url)
            .query(&[("changelist", "0"), ("channel", "main")])
            .send()?;
        assert_eq!(resp.status(), 200);
        assert!(
            resp.headers()
                .get("Content-Type")
                .is_some_and(|t| t == "text/plain")
        );
        assert_eq!(resp.text()?, "");
    }

    // change download
    {
        let resp = client.get(&url).query(&[("change", "abcdef123")]).send()?;
        assert_eq!(resp.status(), 200);
        assert!(
            resp.headers()
                .get("Cache-Control")
                .is_some_and(|v| v == "public, max-age=31536000, immutable")
        );
        let bytes = resp.bytes()?.to_vec();
        assert_eq!(bytes, b"change data");
    }

    // tag download
    {
        let resp = client.get(&url).query(&[("tag", "abcdef123")]).send()?;
        assert_eq!(resp.status(), 200);
        assert!(
            resp.headers()
                .get("Cache-Control")
                .is_some_and(|v| v == "public, max-age=31536000, immutable")
        );
        let bytes = resp.bytes()?.to_vec();
        assert_eq!(bytes, b"tag data");
    }

    // invalid query => 400
    {
        let resp = client.get(&url).query(&[("changelist", "0")]).send()?;
        assert_eq!(resp.status(), 400);
    }

    Ok(())
}

#[test]
fn pijul_cli_clone_and_pull_over_http() -> Result<()> {
    // Remote repo with actual recorded changes, served over HTTP.
    let remote = RepoFixture::new()?;
    remote.write_file("hello.txt", "v1\n")?;

    {
        let mut cmd = std::process::Command::new("pijul");
        cmd.current_dir(remote.repo_root())
            .arg("add")
            .arg("hello.txt");
        remote.apply_env(&mut cmd);
        run_pijul(cmd)?;
    }
    {
        let mut cmd = std::process::Command::new("pijul");
        cmd.current_dir(remote.repo_root())
            .arg("record")
            .arg("--all")
            .arg("--message")
            .arg("init")
            .arg("--identity")
            .arg("test")
            .arg("--no-prompt");
        remote.apply_env(&mut cmd);
        run_pijul(cmd)?;
    }

    let server = TestServer::start(remote)?;

    // Clone into a separate isolated HOME.
    let env = PijulEnv::new()?;
    let clone_parent = assert_fs::TempDir::new()?;
    let clone_dir = clone_parent.child("clone");

    {
        let mut cmd = std::process::Command::new("pijul");
        cmd.current_dir(clone_parent.path())
            .arg("clone")
            .arg(server.base_url())
            .arg(clone_dir.path())
            .arg("--no-prompt");
        env.apply_env(&mut cmd);
        run_pijul(cmd)?;
    }

    clone_dir.child("hello.txt").assert("v1\n");

    // Add another change on the remote while the server is running.
    std::fs::write(server.repo_root().join("hello.txt"), "v2\n")?;
    {
        let mut cmd = std::process::Command::new("pijul");
        cmd.current_dir(server.repo_root())
            .arg("record")
            .arg("--all")
            .arg("--message")
            .arg("update")
            .arg("--identity")
            .arg("test")
            .arg("--no-prompt");
        server.apply_repo_env(&mut cmd);
        run_pijul(cmd)?;
    }

    // Pull into the clone.
    {
        let mut cmd = std::process::Command::new("pijul");
        cmd.current_dir(clone_dir.path())
            .arg("pull")
            .arg(server.base_url())
            .arg("--all")
            .arg("--no-prompt");
        env.apply_env(&mut cmd);
        run_pijul(cmd)?;
    }

    clone_dir.child("hello.txt").assert("v2\n");

    Ok(())
}