#![feature(never_type)]
mod fixtures;

use anyhow::anyhow;
use anyhow::Result;
use chromiumoxide::cdp::browser_protocol::log::EventEntryAdded;
use chromiumoxide::cdp::js_protocol::runtime::EventConsoleApiCalled;
use chromiumoxide::{
    browser::{Browser, BrowserConfig},
    cdp::browser_protocol::{
        network::{EventRequestWillBeSent, EventResponseReceived, Request, Response},
        page::NavigateParams,
    },
    element::Element,
    page::ScreenshotParams,
    Page,
};
use cucumber::World;
use futures::channel::mpsc::Sender;
use futures_util::stream::StreamExt;
use std::sync::LazyLock;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, sync::Arc, time::Duration};
use tokio::sync::RwLock;
use tokio_tungstenite::connect_async;
use uuid::Uuid;
static EMAIL_ID_MAP: LazyLock<RwLock<HashMap<String, String>>> =
    LazyLock::new(|| RwLock::new(HashMap::new()));

#[derive(Clone, Debug, PartialEq)]
pub struct RequestPair {
    req: Option<Request>,
    redirect_resp: Option<Response>,
    resp: Option<Response>,
    cookies_before_request: String,
    cookies_after_response: String,
    ts: std::time::Instant,
}

/*
let screenshot =   world
  .page
  .screenshot(
      ScreenshotParams::builder()
          .capture_beyond_viewport(true)
          .full_page(true)
          .build(),
  )
  .await
  .unwrap();
  world.screenshots.push(screenshot);
   */
#[derive(Clone, Debug)]
pub enum CookieEnum {
    BeforeReq(String),
    AfterResp(String),
}
impl RequestPair {
    pub fn to_string(&self) -> String {
        let (top_req, req_headers) = if let Some(req) = &self.req {
            (
                format!("{} : {} \n", req.method, req.url,),
                format!("{} :\n{:#?} \n", req.url, req.headers),
            )
        } else {
            ("NO REQ".to_string(), "NO REQ".to_string())
        };
        let (top_redirect_resp, _redirect_resp_headers) = if let Some(resp) = &self.redirect_resp {
            (
                format!("{} : {}", resp.status, resp.url),
                format!("{} :\n {:#?}", resp.url, resp.headers),
            )
        } else {
            ("".to_string(), "".to_string())
        };
        let (top_resp, resp_headers) = if let Some(resp) = &self.resp {
            (
                format!("{} : {}", resp.status, resp.url),
                format!("{} :\n {:#?}", resp.url, resp.headers),
            )
        } else {
            ("NO RESP".to_string(), "NO RESP".to_string())
        };

        format!(
            "REQ: {}\n RESP: {}\n \n REDIRECT {} \n REQ_HEADERS: {} \n REQ_COOKIES: \n{}\n RESP_HEADERS:{} \n RESP_COOKIES: \n{}\n ",
            top_req, top_resp,top_redirect_resp,  req_headers,  self.cookies_before_request,resp_headers,self.cookies_after_response
        )
    }
}

#[tokio::main]
async fn main() -> Result<()> {
    // create a thread and store a
    //  tokio-tungstenite client that connectsto http://127.0.0.1:1080/ws
    // and then stores the recieved messages in a std::sync::LazyLock<RwLock<Vec<MailCrabMsg>>>
    // or a custom struct that matches the body or has specific impls for verify codes, links etc.
    let _ = tokio::spawn(async move {
        let (mut socket, _) = connect_async(
            url::Url::parse("ws://127.0.0.1:1080/ws").expect("Can't connect to case count URL"),
        )
        .await
        .unwrap();
        while let Some(msg) = socket.next().await {
            if let Ok(tokio_tungstenite::tungstenite::Message::Text(text)) = msg {
                let Email { id, to } = serde_json::from_str::<Email>(&text).unwrap();
                let email = to[0].email.clone();
                EMAIL_ID_MAP.write().await.insert(email, id.to_string());
            }
        }
    });

    AppWorld::cucumber()
        .init_tracing()
        .fail_on_skipped()
        .max_concurrent_scenarios(1)
        .fail_fast()
        .before(|_feature, _rule, scenario, world| {
            Box::pin(async move {
                let screenshot_directory_name = format!("./screenshots/{}", scenario.name);
                if let Ok(sc_dir) = std::fs::read_dir(&screenshot_directory_name) {
                    for file in sc_dir {
                        if let Ok(file) = file {
                            std::fs::remove_file(file.path()).unwrap();
                        }
                    }
                } else {
                    std::fs::create_dir(&screenshot_directory_name).unwrap();
                }
                // take the page from world
                // add network event listener, tracking requests and pairing them with responses
                // store them somewhere inside of the world.
                let page = world.page.clone();
                let mut req_events = page
                    .event_listener::<EventRequestWillBeSent>()
                    .await
                    .unwrap();
                let mut resp_events = page
                    .event_listener::<EventResponseReceived>()
                    .await
                    .unwrap();
                world.page.enable_log().await.unwrap();
                // get log events generated by the browser
                let mut log_events = page.event_listener::<EventEntryAdded>().await.unwrap();
                // get log events generated by leptos or other console.log() calls..
                let mut runtime_events = page
                    .event_listener::<EventConsoleApiCalled>()
                    .await
                    .unwrap();
                let console_logs = world.console_logs.clone();
                let console_logs_2 = world.console_logs.clone();

                tokio::task::spawn(async move {
                    while let Some(event) = log_events.next().await {
                        if let Some(EventEntryAdded { entry }) =
                        Arc::<EventEntryAdded>::into_inner(event) {
                            console_logs.write().await.push(format!(" {entry:#?} "));
                        } else {
                            tracing::error!("tried to into inner but none")
                        }
                    }
                });

                tokio::task::spawn(async move {
                    while let Some(event) = runtime_events.next().await {
                        if let Some(event) =Arc::<EventConsoleApiCalled>::into_inner(event) {
                            console_logs_2
                            .write()
                            .await
                            .push(format!(" CONSOLE_LOG: {:#?}", event.args));
                        } else {
                            tracing::error!("tried to into inner but none")
                        }

                    }
                });

                let (tx, mut rx) = futures::channel::mpsc::channel::<Option<CookieEnum>>(1000);
                let mut tx_c = tx.clone();
                let mut tx_c_2 = tx.clone();

                world.cookie_sender = Some(tx);
                let req_resp = world.req_resp.clone();
                // Ideally you'd send the message for the Page to get the cookies from inside of the event stream loop,
                // but for some reason that doesn't always work (but sometimes it does),
                // but putting it in it's own thread makes it always work. Not sure why at the moment... ,
                // something about async, about senders, about trying to close the browser but keeping senders around.
                // we need to close the loop and drop the task to close the browser (I think)...
                tokio::task::spawn(async move {
                    while let Some(some_request_id) = rx.next().await {
                        if let Some(cookie_enum) = some_request_id {
                            match cookie_enum {
                                CookieEnum::BeforeReq(req_id) => {
                                    let cookies = page
                                        .get_cookies()
                                        .await
                                        .unwrap_or_default()
                                        .iter()
                                        .map(|cookie| {
                                            format!("name={}\n value={}", cookie.name, cookie.value)
                                        })
                                        .collect::<Vec<String>>()
                                        .join("\n");
                                    if let Some(thing) = req_resp
                                    .write()
                                    .await
                                    .get_mut(&req_id) {
                                        thing.cookies_before_request = cookies;

                                    }

                                }
                                CookieEnum::AfterResp(req_id) => {
                                    let cookies = page
                                        .get_cookies()
                                        .await
                                        .unwrap_or_default()
                                        .iter()
                                        .map(|cookie| {
                                            format!("name={}\n value={}", cookie.name, cookie.value)
                                        })
                                        .collect::<Vec<String>>()
                                        .join("\n");
                                    if let Some(thing) = req_resp
                                    .write()
                                    .await
                                    .get_mut(&req_id) {
                                        thing.cookies_after_response = cookies;
                                    }
                                   }
                            }
                        } else {
                            break;
                        }
                    }
                });

                let req_resp = world.req_resp.clone();
                tokio::task::spawn(async move {
                    while let Some(event) = req_events.next().await {
                        if let Some(event) = Arc::<EventRequestWillBeSent>::into_inner(event) {
                            if event.request.url.contains("/pkg/") {
                                continue;
                            }
                            let req_id = event.request_id.inner().clone();
                            req_resp.write().await.insert(
                                req_id.clone(),
                                RequestPair {
                                    req: Some(event.request),
                                    redirect_resp: event.redirect_response,
                                    resp: None,
                                    cookies_before_request: "".to_string(),
                                    cookies_after_response: "".to_string(),
                                    ts: std::time::Instant::now(),
                                },
                            );
                            if let Err(msg) = tx_c.try_send(Some(CookieEnum::BeforeReq(req_id.clone()))) {
                                tracing::error!(" oopsies on the {msg:#?}");
                            }
                        } else {
                            tracing::error!("into inner err")
                        }
                    }
                });

                let req_resp = world.req_resp.clone();
                tokio::task::spawn(async move {
                    while let Some(event) = resp_events.next().await {
                        if let Some(event) = Arc::<EventResponseReceived>::into_inner(event){
                            if event.response.url.contains("/pkg/") {
                                continue;
                            }
                            let req_id = event.request_id.inner().clone();
                            if let Err(msg) = tx_c_2
                                .try_send(Some(CookieEnum::AfterResp(req_id.clone()))) {
                                tracing::error!("err sending {msg:#?}");
                            }
                            if let Some(request_pair) = req_resp.write().await.get_mut(&req_id) {
                                request_pair.resp = Some(event.response);
                            } else {
                                req_resp.write().await.insert(
                                    req_id.clone(),
                                    RequestPair {
                                        req: None,
                                        redirect_resp: None,
                                        resp: Some(event.response),
                                        cookies_before_request: "No cookie?".to_string(),
                                        cookies_after_response: "No cookie?".to_string(),
                                        ts: std::time::Instant::now(),
                                    },
                                );
                            }
                        } else {
                            tracing::error!(" uhh err here")
                        }


                    }
                });
                // We don't need to join on our join handles, they will run detached and clean up whenever.
            })
        })
        .after(|_feature, _rule, scenario, ev, world| {
            Box::pin(async move {
                let screenshot_directory_name = format!("./screenshots/{}", scenario.name);

                let world = world.unwrap();
                // screenshot the last step
                if let Ok(screenshot) = world
                .page
                .screenshot(
                    ScreenshotParams::builder()
                        .capture_beyond_viewport(true)
                        .full_page(true)
                        .build(),
                )
                .await {
                    world.screenshots.push(screenshot);
                }

                if let cucumber::event::ScenarioFinished::StepFailed(_, _, _) = ev {
                    // close the cookie task.
                    if world
                        .cookie_sender
                        .as_mut()
                        .unwrap()
                        .try_send(None).is_err() {
                            tracing::error!("can't close cookie sender");
                        }
                    // print any applicable screenshots (just the last one of the failed step if there was none taken during the scenario)
                    for (i, screenshot) in world.screenshots.iter().enumerate() {
                        // i.e ./screenshots/login/1.png
                        _ =std::fs::write(
                            screenshot_directory_name.clone()
                                + "/"
                                + i.to_string().as_str()
                                + ".png",
                            screenshot,
                        );
                    }
                    // print network
                    let mut network_output = world
                        .req_resp
                        .read()
                        .await
                        .values()
                        .map(|val| val.clone())
                        .collect::<Vec<RequestPair>>();

                    network_output.sort_by(|a, b| a.ts.cmp(&b.ts));

                    let network_output = network_output
                        .into_iter()
                        .map(|val| val.to_string())
                        .collect::<Vec<String>>()
                        .join("\n");

                    _ = std::fs::write("./network_output", network_output.as_bytes());

                    let console_logs = world.console_logs.read().await.join("\n");

                    _ =std::fs::write("./console_logs", console_logs.as_bytes());

                    // print html
                    if let Ok(html) = world.page.content().await {
                        _ = std::fs::write("./html", html.as_bytes());
                    }
                }
                if let Err(err) = world.browser.close().await {
                    tracing::error!("{err:#?}");
                }
                if let Err(err) =  world.browser.wait().await {
                    tracing::error!("{err:#?}");
                }
            })
        })
        .run_and_exit("./features")
        .await;
    Ok(())
}

#[tracing::instrument]
async fn build_browser() -> Result<Browser, Box<dyn std::error::Error>> {
    let (browser, mut handler) = Browser::launch(
        BrowserConfig::builder()
            //.enable_request_intercept()
            .disable_cache()
            .request_timeout(Duration::from_secs(1))
            //.with_head()
            //.arg("--remote-debugging-port=9222")
            .build()?,
    )
    .await?;

    tokio::task::spawn(async move {
        while let Some(h) = handler.next().await {
            if h.is_err() {
                tracing::info!("{h:?}");
                break;
            }
        }
    });

    Ok(browser)
}

pub const HOST: &str = "https://127.0.0.1:3000";

#[derive(World)]
#[world(init = Self::new)]
pub struct AppWorld {
    pub browser: Browser,
    pub page: Page,
    pub req_resp: Arc<RwLock<HashMap<String, RequestPair>>>,
    pub clipboard: HashMap<&'static str, String>,
    pub cookie_sender: Option<Sender<Option<CookieEnum>>>,
    pub screenshots: Vec<Vec<u8>>,
    pub console_logs: Arc<RwLock<Vec<String>>>,
}

impl std::fmt::Debug for AppWorld {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("AppWorld").finish()
    }
}

impl AppWorld {
    async fn new() -> Result<Self, anyhow::Error> {
        let browser = build_browser().await.unwrap();

        let page = browser.new_page("about:blank").await?;

        Ok(Self {
            browser,
            page,
            req_resp: Arc::new(RwLock::new(HashMap::new())),
            clipboard: HashMap::new(),
            cookie_sender: None,
            screenshots: Vec::new(),
            console_logs: Arc::new(RwLock::new(Vec::new())),
        })
    }

    pub async fn errors(&mut self) -> Result<()> {
        if let Ok(error) = self.find(ids::ERROR_ERROR_ID).await {
            Err(anyhow!("{}", error.inner_text().await?.unwrap_or(String::from("no error in inner template?"))))
        } else {
            Ok(())
        }
    }

    pub async fn find(&self, id: &'static str) -> Result<Element> {
        for _ in 0..4 {
            if let Ok(el) = self.page.find_element(format!("#{id}")).await {
                return Ok(el);
            }
            crate::fixtures::wait().await;
        }
        Err(anyhow!("Can't find {id}"))
    }

    pub async fn find_submit(&mut self) -> Result<Element> {
        for _ in 0..4 {
            if let Ok(el) = self.page.find_element(format!("input[type=submit]")).await {
                return Ok(el);
            }
            crate::fixtures::wait().await;
        }
        Err(anyhow!("Can't find input type=submit"))
    }

    /*pub async fn find_all(&mut self, id: &'static str) -> Result<ElementList> {
        Ok(ElementList(
            self.page.find_elements(format!("#{id}")).await?,
        ))
    }*/

    pub async fn goto_url(&mut self, url: &str) -> Result<()> {
        self.page
            .goto(
                NavigateParams::builder()
                    .url(url)
                    .build()
                    .map_err(|err| anyhow!(err))?,
            )
            .await?
            .wait_for_navigation()
            .await?;
        self.screenshot().await?;
        Ok(())
    }

    pub async fn goto_path(&mut self, path: &str) -> Result<()> {
        let url = format!("{}{}", HOST, path);
        self.page
            .goto(
                NavigateParams::builder()
                    .url(url)
                    .build()
                    .map_err(|err| anyhow!(err))?,
            )
            .await?;
        self.screenshot().await?;
        Ok(())
    }
    pub async fn screenshot(&mut self) -> Result<()> {
        let sc = self.page.screenshot(ScreenshotParams::default()).await?;
        self.screenshots.push(sc);
        Ok(())
    }
    pub async fn set_field<S: AsRef<str> + std::fmt::Display>(
        &mut self,
        id: &'static str,
        value: S,
    ) -> Result<()> {
        let element = self.find(id).await?;
        element.focus().await?.type_str(value).await?;
        self.screenshot().await?;
        Ok(())
    }

    pub async fn click(&mut self, id: &'static str) -> Result<()> {
        self.find(id).await?.click().await?;
        Ok(())
    }
    #[tracing::instrument(err)]
    pub async fn submit(&mut self) -> Result<()> {
        self.screenshot().await?;
        self.find_submit().await?.click().await?;
        Ok(())
    }
    pub async fn find_text(&self, text: String) -> Result<Element> {
        let selector: String = format!("//*[contains(text(), '{text}') or @*='{text}']");
        let mut count = 0;
        loop {
            let result = self.page.find_xpath(&selector).await;
            if result.is_err() && count < 4 {
                count += 1;
                crate::fixtures::wait().await;
            } else {
                let result = result?;
                return Ok(result);
            }
        }
    }
    pub async fn url_contains(&self, s: &'static str) -> Result<()> {
        if let Some(current) = self.page.url().await? {
            if !current.contains(s) {
                return Err(anyhow!("{current} does not contains {s}"));
            }
        } else {
            return Err(anyhow!("NO CURRENT URL FOUND"));
        }
        Ok(())
    }
    pub async fn verify_route(&self, path: &'static str) -> Result<()> {
        let url = format!("{}{}", HOST, path);
        if let Some(current) = self.page.url().await? {
            if current != url {
                return Err(anyhow!(
                    "EXPECTING ROUTE: {path}\n but FOUND:\n {current:#?}"
                ));
            }
        } else {
            return Err(anyhow!(
                "EXPECTING ROUTE: {path}\n but NO CURRENT URL FOUND"
            ));
        }
        Ok(())
    }
}

/*
#[derive(Debug)]
pub struct ElementList(Vec<Element>);
impl ElementList {
    /// iterates over elements, finds first element whose text (as rendered) contains text given as function's argument.
    pub async fn find_by_text(&self,text:&'static str) -> Result<Element> {
        for element in self.0.iter() {
            if let Ok(Some(inner_text)) = element.inner_text().await {
                if inner_text.contains(text) {
                    return Ok(element);
                }
            }
        }
        Err(anyhow!(format!("given text {} no element found",text)))
    }

}*/

#[derive(Serialize, Deserialize, Debug)]
struct Email {
    id: Uuid,
    to: Vec<Recipient>,
}

#[derive(Serialize, Deserialize, Debug)]
struct Recipient {
    name: Option<String>,
    email: String,
}
