use std::net::{SocketAddr, TcpListener, TcpStream};
use std::process::Command;
use std::thread;
use std::time::{Duration, Instant};

use pijul_srv_lite::server::{ServerConfig, resolve_repo_path, serve};

use anyhow::{Context, Result, bail};

use crate::support::RepoFixture;

pub struct TestServer {
    port: u16,
    _repo: RepoFixture,
}

const BIND_ADDR: &str = "127.0.0.1";

impl TestServer {
    pub fn start(repo: RepoFixture) -> Result<Self> {
        let repo_path = resolve_repo_path(repo.repo_root().to_path_buf())?;

        // Binds to port 0, which asks OS for port
        let listener =
            TcpListener::bind("127.0.0.1:0").context("failed to bind ephemeral test listener")?;

        let port = listener
            .local_addr()
            .context("failed to read ephemeral listener address")?
            .port();

        thread::spawn(move || {
            if let Err(err) = serve(listener, ServerConfig { repo_path }) {
                eprintln!("Server exited with error: {err:#}");
            }
        });

        wait_until_ready(port).context("server did not become ready")?;

        Ok(Self { port, _repo: repo })
    }

    pub fn base_url(&self) -> String {
        format!("http://{BIND_ADDR}:{}", self.port)
    }

    pub fn repo_root(&self) -> &std::path::Path {
        self._repo.repo_root()
    }

    pub fn apply_repo_env(&self, cmd: &mut Command) {
        self._repo.apply_env(cmd);
    }
}

fn wait_until_ready(port: u16) -> Result<()> {
    let addr: SocketAddr = ([127, 0, 0, 1], port).into();
    let deadline = Instant::now() + Duration::from_secs(5);

    loop {
        if TcpStream::connect_timeout(&addr, Duration::from_millis(50)).is_ok() {
            return Ok(());
        }

        if Instant::now() >= deadline {
            bail!("timed out waiting for server to listen on {addr}",);
        }

        std::thread::sleep(Duration::from_millis(20));
    }
}