use super::build::{trace_filter_parser, trace_level_parser};
use aiken_lang::{
    ast::{TraceLevel, Tracing},
    test_framework::PropertyTest,
};
use aiken_project::{
    telemetry::json_schema,
    watch::{self, watch_project, with_project},
};
use rand::prelude::*;
use std::{
    io::{self, IsTerminal},
    path::PathBuf,
    process,
};

#[derive(clap::Args)]
#[command(
    verbatim_doc_comment,
    about = color_print::cstr!(r#"
Type-check an Aiken project and run any tests found.

Test results are printed as stylized outputs when `stdout` is a TTY-capable terminal. If it
isn't, (e.g. because you are redirecting the output to a file), test results are printed as
a JSON structured object. Use `--show-json-schema` to see the whole schema.
"#),
    after_long_help = color_print::cstr!(r#"You are seeing the extended help. Use `-h` instead of `--help` for a more compact view.
"#
))]
pub struct Args {
    /// Path to project
    directory: Option<PathBuf>,

    /// Deny warnings; warnings will be treated as errors
    #[clap(short = 'D', long)]
    deny: bool,

    /// Silence warnings; warnings will not be printed
    #[clap(short = 'S', long)]
    silent: bool,

    /// Skip tests; run only the type-checker
    #[clap(short, long)]
    skip_tests: bool,

    /// When enabled, also pretty-print test UPLC on failure
    #[clap(long)]
    debug: bool,

    /// When enabled, print-out the JSON-schema of the command output when the target isn't an
    /// ANSI-capable terminal
    #[clap(long, required = false)]
    show_json_schema: bool,

    /// When enabled, re-run the command on file changes instead of exiting
    #[clap(long)]
    watch: bool,

    /// An initial seed to initialize the pseudo-random generator for property-tests.
    #[clap(long, value_name = "UINT")]
    seed: Option<u32>,

    /// Maximum number of successful test run for considering a property-based test valid.
    #[clap(long, default_value_t = PropertyTest::DEFAULT_MAX_SUCCESS, value_name="UINT")]
    max_success: usize,

    /// Only run tests if they match any of these strings.
    /// You can match a module with `-m aiken/list` or `-m list`.
    /// You can match a test with `-m "aiken/list.{map}"` or `-m "aiken/option.{flatten_1}"`
    #[clap(short, long, verbatim_doc_comment)]
    match_tests: Option<Vec<String>>,

    /// This is meant to be used with `--match-tests`.
    /// It forces test names to match exactly
    #[clap(short, long)]
    exact_match: bool,

    /// Environment to build against.
    #[clap(long)]
    env: Option<String>,

    /// Filter traces to be included in the generated program(s).
    ///
    ///   - user-defined:
    ///       only consider traces that you've explicitly defined
    ///       either through the 'trace' keyword of via the trace-if-false
    ///       ('?') operator.
    ///
    ///   - compiler-generated:
    ///       only included internal traces generated by the
    ///       Aiken compiler, for example in usage of 'expect'.
    ///
    ///   - all:
    ///       include both user-defined and compiler-generated traces.
    ///
    /// [default: all]
    #[clap(short = 'f', long, value_parser=trace_filter_parser(), default_missing_value="all", verbatim_doc_comment, alias="filter_traces")]
    trace_filter: Option<fn(TraceLevel) -> Tracing>,

    /// Choose the verbosity level of traces:
    ///
    ///   - silent: disable traces altogether
    ///   - compact: only culprit line numbers are shown on failures
    ///   - verbose: enable full verbose traces as provided by the user or the compiler
    ///
    /// [optional]
    #[clap(short, long, value_parser=trace_level_parser(), default_value_t=TraceLevel::Verbose, verbatim_doc_comment)]
    trace_level: TraceLevel,
}

pub fn exec(
    Args {
        directory,
        deny,
        silent,
        skip_tests,
        debug,
        show_json_schema,
        match_tests,
        exact_match,
        watch,
        trace_filter,
        trace_level,
        seed,
        max_success,
        env,
    }: Args,
) -> miette::Result<()> {
    if show_json_schema {
        println!("{}", serde_json::to_string_pretty(&json_schema()).unwrap());
        std::process::exit(0);
    }

    let mut rng = rand::thread_rng();

    let seed = seed.unwrap_or_else(|| rng.gen());

    let result = if watch {
        watch_project(directory.as_deref(), watch::default_filter, 500, |p| {
            p.check(
                skip_tests,
                match_tests.clone(),
                debug,
                exact_match,
                seed,
                max_success,
                match trace_filter {
                    Some(trace_filter) => trace_filter(trace_level),
                    None => Tracing::All(trace_level),
                },
                env.clone(),
            )
        })
    } else {
        with_project(
            directory.as_deref(),
            deny,
            silent,
            !io::stdout().is_terminal(),
            |p| {
                p.check(
                    skip_tests,
                    match_tests.clone(),
                    debug,
                    exact_match,
                    seed,
                    max_success,
                    match trace_filter {
                        Some(trace_filter) => trace_filter(trace_level),
                        None => Tracing::All(trace_level),
                    },
                    env.clone(),
                )
            },
        )
    };

    result.map_err(|_| process::exit(1))
}
