/*
 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
 *
 * SPDX-License-Identifier: LicenseRef-SEL
 *
 * This file is subject to the Stalwart Enterprise License Agreement (SEL) and
 * is NOT open source software.
 *
 */

use crate::{
    AssertConfig,
    directory::internal::TestInternalDirectory,
    imap::{ImapConnection, Type},
    jmap::{
        JMAPTest, ManagementApi,
        mail::delivery::{AssertResult, SmtpConnection},
        server::List,
        wait_for_index,
    },
};
use common::{
    Core, Server,
    config::telemetry::{StoreTracer, TelemetrySubscriberType},
    core::BuildServer,
    enterprise::{
        Enterprise, MetricStore, TraceStore, Undelete, config::parse_metric_alerts,
        license::LicenseKey,
    },
    telemetry::{
        metrics::store::{Metric, MetricsStore, SharedMetricHistory},
        tracers::store::TracingStore,
    },
};
use directory::{QueryBy, backend::internal::manage::ManageDirectory};
use http::management::{
    enterprise::undelete::{
        DeletedBlobResponse, DeletedItemResponse, UndeleteRequest, UndeleteResponse,
    },
    stores::destroy_account_data,
};
use imap_proto::ResponseType;
use nlp::language::Language;
use std::{sync::Arc, time::Duration};
use store::{
    rand::{self, Rng},
    search::{
        SearchField, SearchFilter, SearchOperator, SearchQuery, SearchValue, TracingSearchField,
    },
    write::{SearchIndex, now},
};
use trc::{
    ipc::{bitset::Bitset, subscriber::SubscriberBuilder},
    *,
};
use utils::config::{Config, cron::SimpleCron};

const METRICS_CONFIG: &str = r#"
[metrics.alerts.expected]
enable = true
condition = "domain_count > 1 && cluster_publisher_error > 3"

[metrics.alerts.expected.notify.event]
enable = true
message = "Yikes! Found %{cluster.publisher-error}% cluster errors!"

[metrics.alerts.expected.notify.email]
enable = true
from-name = "Alert Subsystem"
from-addr = "alert@example.com"
to = ["jdoe@example.com"]
subject = "Found %{cluster.publisher-error}% cluster errors"
body = "Sorry for the bad news, but we found %{domain.count}% domains and %{cluster.publisher-error}% cluster errors."

[metrics.alerts.unexpected]
enable = true
condition = "domain_count < 1 || cluster_publisher_error < 3"

[metrics.alerts.unexpected.notify.event]
enable = true
message = "this should not have happened"

"#;

const RAW_MESSAGE: &str = "From: john@example.com
To: john@example.com
Subject: undelete test

test
";

pub async fn test(params: &mut JMAPTest) {
    // Enable Enterprise
    println!("Running Enterprise tests...");
    let mut core = params.server.inner.shared_core.load_full().as_ref().clone();
    let mut config = Config::new(METRICS_CONFIG).unwrap();
    core.enterprise = Enterprise {
        license: LicenseKey {
            valid_to: now() + 3600,
            valid_from: now() - 3600,
            domain: String::new(),
            accounts: 100,
        },
        undelete: Undelete {
            retention: Duration::from_secs(2),
        }
        .into(),
        trace_store: TraceStore {
            retention: Some(Duration::from_secs(1)),
            store: core.storage.data.clone(),
        }
        .into(),
        metrics_store: MetricStore {
            retention: Some(Duration::from_secs(1)),
            store: core.storage.data.clone(),
            interval: SimpleCron::Day { hour: 0, minute: 0 },
        }
        .into(),
        metrics_alerts: parse_metric_alerts(&mut config),
        logo_url: None,
        ai_apis: Default::default(),
        spam_filter_llm: None,
        template_calendar_alarm: None,
        template_scheduling_email: None,
        template_scheduling_web: None,
    }
    .into();
    config.assert_no_errors();
    assert_ne!(core.enterprise.as_ref().unwrap().metrics_alerts.len(), 0);
    params.server.inner.shared_core.store(core.into());
    assert!(
        params
            .server
            .inner
            .shared_core
            .load()
            .is_enterprise_edition()
    );

    // Create test account
    let server = params.server.inner.build_server();
    let account_id = server
        .store()
        .create_test_user(
            "jdoe@example.com",
            "12345",
            "John Doe",
            &["jdoe@example.com"],
        )
        .await;

    alerts(&server).await;
    undelete(params).await;
    tracing(params).await;
    metrics(params).await;

    // Delete test account
    server
        .store()
        .delete_principal(QueryBy::Id(account_id))
        .await
        .unwrap();
    destroy_account_data(&server, account_id, true)
        .await
        .unwrap();
    params.assert_is_empty().await;

    params.server.inner.shared_core.store(
        params
            .server
            .inner
            .shared_core
            .load_full()
            .as_ref()
            .clone()
            .enable_enterprise()
            .into(),
    );
}

pub trait EnterpriseCore {
    fn enable_enterprise(self) -> Self;
}

impl EnterpriseCore for Core {
    fn enable_enterprise(mut self) -> Self {
        self.enterprise = Enterprise {
            license: LicenseKey {
                valid_to: now() + 3600,
                valid_from: now() - 3600,
                domain: String::new(),
                accounts: 100,
            },
            undelete: None,
            trace_store: None,
            metrics_store: None,
            metrics_alerts: vec![],
            logo_url: None,
            ai_apis: Default::default(),
            spam_filter_llm: None,
            template_calendar_alarm: None,
            template_scheduling_email: None,
            template_scheduling_web: None,
        }
        .into();
        self
    }
}

async fn alerts(server: &Server) {
    // Make sure the required metrics are set to 0
    assert_eq!(
        Collector::read_event_metric(EventType::Cluster(ClusterEvent::PublisherError).id()),
        0
    );
    assert_eq!(Collector::read_metric(MetricType::DomainCount), 0.0);
    assert_eq!(
        Collector::read_event_metric(EventType::Telemetry(TelemetryEvent::Alert).id()),
        0
    );

    // Increment metrics to trigger alerts
    Collector::update_event_counter(EventType::Cluster(ClusterEvent::PublisherError), 5);
    Collector::update_gauge(MetricType::DomainCount, 3);

    // Make sure the values were set
    assert_eq!(
        Collector::read_event_metric(EventType::Cluster(ClusterEvent::PublisherError).id()),
        5
    );
    assert_eq!(Collector::read_metric(MetricType::DomainCount), 3.0);

    // Process alerts
    let message = server.process_alerts().await.unwrap().pop().unwrap();
    assert_eq!(message.from, "alert@example.com");
    assert_eq!(message.to, vec!["jdoe@example.com".to_string()]);
    let body = String::from_utf8(message.body).unwrap();
    assert!(
        body.contains("Sorry for the bad news, but we found 3 domains and 5 cluster errors."),
        "{body:?}"
    );
    assert!(body.contains("Subject: Found 5 cluster errors"), "{body:?}");
    assert!(
        body.contains("From: \"Alert Subsystem\" <alert@example.com>"),
        "{body:?}"
    );
    assert!(body.contains("To: <jdoe@example.com>"), "{body:?}");

    // Make sure the event was triggered
    assert_eq!(
        Collector::read_event_metric(EventType::Telemetry(TelemetryEvent::Alert).id()),
        1
    );
}

async fn tracing(params: &mut JMAPTest) {
    // Enable tracing
    let store = params.server.core.storage.data.clone();
    let query = params.server.core.storage.fts.clone();
    TelemetrySubscriberType::StoreTracer(StoreTracer {
        store: store.clone(),
    })
    .spawn(
        SubscriberBuilder::new("store-tracer".to_string()).with_interests(Box::new(Bitset::all())),
        true,
    );

    // Make sure there are no span entries in the db
    store
        .purge_spans(Duration::from_secs(0), Some(&query))
        .await
        .unwrap();
    assert_eq!(
        query
            .query_global(SearchQuery::new(SearchIndex::Tracing).with_filters(vec![
                SearchFilter::Operator {
                    field: SearchField::Tracing(TracingSearchField::EventType),
                    op: SearchOperator::Equal,
                    value: SearchValue::Uint(EventType::Smtp(SmtpEvent::ConnectionStart).code())
                }
            ]))
            .await
            .unwrap(),
        Vec::<u64>::new()
    );

    // Send an email
    let mut lmtp = SmtpConnection::connect().await;
    lmtp.ingest(
        "bill@example.com",
        &["jdoe@example.com"],
        concat!(
            "From: bill@example.com\r\n",
            "To: jdoe@example.com\r\n",
            "Subject: TPS Report\r\n",
            "X-Spam-Status: No\r\n",
            "\r\n",
            "I'm going to need those TPS reports ASAP. ",
            "So, if you could do that, that'd be great."
        ),
    )
    .await;
    lmtp.quit().await;
    tokio::time::sleep(Duration::from_millis(300)).await;

    params.server.notify_task_queue();
    wait_for_index(&params.server).await;

    // Purge should not delete anything at this point
    store
        .purge_spans(Duration::from_secs(2), Some(&query))
        .await
        .unwrap();

    // There should be a span entry in the db
    for span_type in [
        EventType::Delivery(DeliveryEvent::AttemptStart),
        EventType::Smtp(SmtpEvent::ConnectionStart),
    ] {
        let spans = query
            .query_global(SearchQuery::new(SearchIndex::Tracing).with_filters(vec![
                SearchFilter::Operator {
                    field: SearchField::Tracing(TracingSearchField::EventType),
                    op: SearchOperator::Equal,
                    value: SearchValue::Uint(span_type.code()),
                },
            ]))
            .await
            .unwrap();
        assert_eq!(spans.len(), 1, "{span_type:?}");
        assert_eq!(
            store.get_span(spans[0]).await.unwrap()[0].inner.typ,
            span_type
        );
    }

    // Try searching
    for keyword in ["bill@example.com", "jdoe@example.com", "example.com"] {
        let spans = query
            .query_global(SearchQuery::new(SearchIndex::Tracing).with_filters(vec![
                SearchFilter::Operator {
                    field: SearchField::Tracing(TracingSearchField::Keywords),
                    op: SearchOperator::Equal,
                    value: SearchValue::Text {
                        value: keyword.to_string(),
                        language: Language::None,
                    },
                },
            ]))
            .await
            .unwrap();

        assert_eq!(spans.len(), 2, "keyword: {keyword}");
        assert!(spans[0] != spans[1], "keyword: {keyword}");
    }

    // Purge should delete the span entries
    tokio::time::sleep(Duration::from_millis(800)).await;
    store
        .purge_spans(Duration::from_secs(1), Some(&query))
        .await
        .unwrap();

    assert_eq!(
        query
            .query_global(SearchQuery::new(SearchIndex::Tracing).with_filters(vec![
                SearchFilter::Operator {
                    field: SearchField::Id,
                    op: SearchOperator::GreaterThan,
                    value: SearchValue::Uint(0),
                },
            ]))
            .await
            .unwrap(),
        Vec::<u64>::new()
    );
}

async fn metrics(params: &mut JMAPTest) {
    // Make sure there are no span entries in the db
    let store = params.server.core.storage.data.clone();
    assert_eq!(
        store.query_metrics(0, u64::MAX).await.unwrap(),
        Vec::<Metric<EventType, MetricType, u64>>::new()
    );

    insert_test_metrics(params.server.core.clone()).await;

    let total = store.query_metrics(0, u64::MAX).await.unwrap();
    assert!(!total.is_empty(), "{total:?}");

    store.purge_metrics(Duration::from_secs(0)).await.unwrap();
    assert_eq!(
        store.query_metrics(0, u64::MAX).await.unwrap(),
        Vec::<Metric<EventType, MetricType, u64>>::new()
    );
}

async fn undelete(params: &mut JMAPTest) {
    // Authenticate
    let mut imap = ImapConnection::connect(b"_x ").await;
    imap.authenticate("jdoe@example.com", "12345").await;

    // Insert test message
    imap.send("STATUS INBOX (MESSAGES)").await;
    imap.assert_read(Type::Tagged, ResponseType::Ok)
        .await
        .assert_contains("MESSAGES 0");
    imap.send(&format!("APPEND INBOX {{{}}}", RAW_MESSAGE.len()))
        .await;
    imap.assert_read(Type::Continuation, ResponseType::Ok).await;
    imap.send_untagged(RAW_MESSAGE).await;
    imap.assert_read(Type::Tagged, ResponseType::Ok).await;

    // Make sure the message is there
    imap.send("STATUS INBOX (MESSAGES)").await;
    imap.assert_read(Type::Tagged, ResponseType::Ok)
        .await
        .assert_contains("MESSAGES 1");
    imap.send("SELECT INBOX").await;
    imap.assert_read(Type::Tagged, ResponseType::Ok).await;

    // Fetch message body
    imap.send("FETCH 1 BODY[]").await;
    imap.assert_read(Type::Tagged, ResponseType::Ok)
        .await
        .assert_contains("Subject: undelete test");

    // Delete and expunge message
    imap.send("STORE 1 +FLAGS (\\Deleted)").await;
    imap.assert_read(Type::Tagged, ResponseType::Ok).await;
    imap.send("EXPUNGE").await;
    imap.assert_read(Type::Tagged, ResponseType::Ok).await;

    // Logout and reconnect
    imap.send("LOGOUT").await;
    imap.assert_read(Type::Tagged, ResponseType::Ok).await;
    let mut imap = ImapConnection::connect(b"_x ").await;
    imap.authenticate("jdoe@example.com", "12345").await;

    // Make sure the message is gone
    imap.send("STATUS INBOX (MESSAGES)").await;
    imap.assert_read(Type::Tagged, ResponseType::Ok)
        .await
        .assert_contains("MESSAGES 0");

    // Query undelete API
    let api = ManagementApi::new(8899, "admin", "secret");
    api.get::<serde_json::Value>("/api/store/purge/account/jdoe@example.com")
        .await
        .unwrap();
    wait_for_index(&params.server).await;
    tokio::time::sleep(Duration::from_millis(200)).await;
    let deleted = api
        .get::<List<DeletedBlobResponse>>("/api/store/undelete/jdoe@example.com")
        .await
        .unwrap()
        .unwrap_data()
        .items;
    assert_eq!(deleted.len(), 1);
    let deleted = deleted.into_iter().next().unwrap();
    match deleted.item {
        DeletedItemResponse::Email { from, subject, .. } => {
            assert_eq!(subject.as_ref(), "undelete test");
            assert_eq!(from.as_ref(), "john@example.com");
        }
        other => {
            panic!("Unexpected deleted item response: {:?}", other);
        }
    }

    // Undelete
    let result = api
        .post::<Vec<UndeleteResponse>>(
            "/api/store/undelete/jdoe@example.com",
            &vec![UndeleteRequest {
                hash: deleted.hash,
                collection: "email".to_string(),
                time: deleted.deleted_at,
                cancel_deletion: deleted.expires_at.into(),
            }],
        )
        .await
        .unwrap()
        .unwrap_data();
    assert_eq!(result, vec![UndeleteResponse::Success]);

    // Make sure the message is back
    imap.send("STATUS INBOX (MESSAGES)").await;
    imap.assert_read(Type::Tagged, ResponseType::Ok)
        .await
        .assert_contains("MESSAGES 1");

    imap.send("SELECT INBOX").await;
    imap.assert_read(Type::Tagged, ResponseType::Ok).await;

    // Fetch message body
    imap.send("FETCH 1 BODY[]").await;
    imap.assert_read(Type::Tagged, ResponseType::Ok)
        .await
        .assert_contains("Subject: undelete test");
}

pub async fn insert_test_metrics(core: Arc<Core>) {
    let store = core.storage.data.clone();
    store.purge_metrics(Duration::from_secs(0)).await.unwrap();
    let mut start_time = now() - (90 * 24 * 60 * 60);
    let timestamp = now();
    let history = SharedMetricHistory::default();

    while start_time < timestamp {
        for event_type in [
            EventType::Smtp(SmtpEvent::ConnectionStart),
            EventType::Imap(ImapEvent::ConnectionStart),
            EventType::Pop3(Pop3Event::ConnectionStart),
            EventType::ManageSieve(ManageSieveEvent::ConnectionStart),
            EventType::Http(HttpEvent::ConnectionStart),
            EventType::Delivery(DeliveryEvent::AttemptStart),
            EventType::Queue(QueueEvent::QueueMessage),
            EventType::Queue(QueueEvent::QueueMessageAuthenticated),
            EventType::Queue(QueueEvent::QueueDsn),
            EventType::Queue(QueueEvent::QueueReport),
            EventType::MessageIngest(MessageIngestEvent::Ham),
            EventType::MessageIngest(MessageIngestEvent::Spam),
            EventType::Auth(AuthEvent::Failed),
            EventType::Security(SecurityEvent::AuthenticationBan),
            EventType::Security(SecurityEvent::ScanBan),
            EventType::Security(SecurityEvent::AbuseBan),
            EventType::Security(SecurityEvent::LoiterBan),
            EventType::Security(SecurityEvent::IpBlocked),
            EventType::IncomingReport(IncomingReportEvent::DmarcReport),
            EventType::IncomingReport(IncomingReportEvent::DmarcReportWithWarnings),
            EventType::IncomingReport(IncomingReportEvent::TlsReport),
            EventType::IncomingReport(IncomingReportEvent::TlsReportWithWarnings),
        ] {
            // Generate a random value between 0 and 100
            Collector::update_event_counter(event_type, rand::rng().random_range(0..=100))
        }

        Collector::update_gauge(MetricType::QueueCount, rand::rng().random_range(0..=1000));
        Collector::update_gauge(
            MetricType::ServerMemory,
            rand::rng().random_range(100 * 1024 * 1024..=300 * 1024 * 1024),
        );

        for metric_type in [
            MetricType::MessageIngestionTime,
            MetricType::MessageFtsIndexTime,
            MetricType::DeliveryTime,
            MetricType::DnsLookupTime,
        ] {
            Collector::update_histogram(metric_type, rand::rng().random_range(2..=1000))
        }
        Collector::update_histogram(
            MetricType::DeliveryTotalTime,
            rand::rng().random_range(1000..=5000),
        );

        store
            .write_metrics(core.clone(), start_time, history.clone())
            .await
            .unwrap();
        start_time += 60 * 60;
    }
}
