use std::str::FromStr;

use solana_program::{program_error::ProgramError, pubkey::Pubkey};
use solana_program_test::{processor, ProgramTest};

use solana_sdk::{signature::Keypair, signer::Signer};
use spl_governance::{
    instruction::{
        create_governance, create_proposal, create_realm, create_token_owner_record,
        deposit_governing_tokens,
    },
    state::{
        enums::{MintMaxVoterWeightSource, VoteThreshold},
        governance::{get_governance_address, GovernanceConfig},
        proposal::{get_proposal_address, VoteType},
        realm::{get_realm_address, GoverningTokenConfigAccountArgs},
        realm_config::GoverningTokenType,
        token_owner_record::get_token_owner_record_address,
    },
};
use spl_governance_addin_mock::instruction::setup_voter_weight_record;
use spl_governance_chat::{
    instruction::post_message,
    processor::process_instruction,
    state::{ChatMessage, GovernanceChatAccountType, MessageBody},
};
use spl_governance_test_sdk::{addins::ensure_addin_mock_is_built, ProgramTestBench};

use crate::program_test::cookies::{ChatMessageCookie, ProposalCookie};

use self::cookies::TokenOwnerRecordCookie;

pub mod cookies;

pub struct GovernanceChatProgramTest {
    pub bench: ProgramTestBench,
    pub program_id: Pubkey,
    pub governance_program_id: Pubkey,
    pub voter_weight_addin_id: Option<Pubkey>,
}

impl GovernanceChatProgramTest {
    #[allow(dead_code)]
    pub async fn start_new() -> Self {
        Self::start_impl(false).await
    }

    #[allow(dead_code)]
    pub async fn start_with_voter_weight_addin() -> Self {
        ensure_addin_mock_is_built();
        Self::start_impl(true).await
    }

    #[allow(dead_code)]
    async fn start_impl(use_voter_weight_addin: bool) -> Self {
        let mut program_test = ProgramTest::default();

        let program_id = Pubkey::from_str("GovernanceChat11111111111111111111111111111").unwrap();
        program_test.add_program(
            "spl_governance_chat",
            program_id,
            processor!(process_instruction),
        );

        let governance_program_id =
            Pubkey::from_str("Governance111111111111111111111111111111111").unwrap();
        program_test.add_program(
            "spl_governance",
            governance_program_id,
            processor!(spl_governance::processor::process_instruction),
        );

        let voter_weight_addin_id = if use_voter_weight_addin {
            let voter_weight_addin_id =
                Pubkey::from_str("VoterWeightAddin111111111111111111111111111").unwrap();
            program_test.add_program("spl_governance_addin_mock", voter_weight_addin_id, None);
            Some(voter_weight_addin_id)
        } else {
            None
        };

        let bench = ProgramTestBench::start_new(program_test).await;

        Self {
            bench,
            program_id,
            governance_program_id,
            voter_weight_addin_id,
        }
    }

    #[allow(dead_code)]
    pub async fn with_proposal(&mut self) -> ProposalCookie {
        // Create Realm
        let name = self.bench.get_unique_name("realm");

        let realm_address = get_realm_address(&self.governance_program_id, &name);

        let governing_token_mint_keypair = Keypair::new();
        let governing_token_mint_authority = Keypair::new();

        self.bench
            .create_mint(
                &governing_token_mint_keypair,
                &governing_token_mint_authority.pubkey(),
                None,
            )
            .await;

        let realm_authority = Keypair::new();

        let community_token_config_args = GoverningTokenConfigAccountArgs {
            voter_weight_addin: self.voter_weight_addin_id,
            max_voter_weight_addin: None,
            token_type: GoverningTokenType::default(),
        };

        let create_realm_ix = create_realm(
            &self.governance_program_id,
            &realm_authority.pubkey(),
            &governing_token_mint_keypair.pubkey(),
            &self.bench.payer.pubkey(),
            None,
            Some(community_token_config_args),
            None,
            name.clone(),
            1,
            MintMaxVoterWeightSource::FULL_SUPPLY_FRACTION,
        );

        self.bench
            .process_transaction(&[create_realm_ix], None)
            .await
            .unwrap();

        // Create TokenOwnerRecord
        let token_owner = Keypair::new();
        let token_source = Keypair::new();
        let amount = 100;

        if self.voter_weight_addin_id.is_none() {
            let transfer_authority = Keypair::new();

            self.bench
                .create_token_account_with_transfer_authority(
                    &token_source,
                    &governing_token_mint_keypair.pubkey(),
                    &governing_token_mint_authority,
                    amount,
                    &token_owner,
                    &transfer_authority.pubkey(),
                )
                .await;

            let deposit_governing_tokens_ix = deposit_governing_tokens(
                &self.governance_program_id,
                &realm_address,
                &token_source.pubkey(),
                &token_owner.pubkey(),
                &token_owner.pubkey(),
                &self.bench.payer.pubkey(),
                amount,
                &governing_token_mint_keypair.pubkey(),
            );

            self.bench
                .process_transaction(&[deposit_governing_tokens_ix], Some(&[&token_owner]))
                .await
                .unwrap();
        } else {
            let deposit_governing_tokens_ix = create_token_owner_record(
                &self.governance_program_id,
                &realm_address,
                &token_owner.pubkey(),
                &governing_token_mint_keypair.pubkey(),
                &self.bench.payer.pubkey(),
            );

            self.bench
                .process_transaction(&[deposit_governing_tokens_ix], None)
                .await
                .unwrap();
        }

        // Create Governance
        let governed_account_address = Pubkey::new_unique();

        let governance_config = GovernanceConfig {
            min_community_weight_to_create_proposal: 5,
            min_council_weight_to_create_proposal: 2,
            min_transaction_hold_up_time: 10,
            max_voting_time: 10,
            community_vote_threshold: VoteThreshold::YesVotePercentage(60),
            community_vote_tipping: spl_governance::state::enums::VoteTipping::Strict,
            council_vote_threshold: VoteThreshold::YesVotePercentage(10),
            council_veto_vote_threshold: VoteThreshold::YesVotePercentage(50),
            council_vote_tipping: spl_governance::state::enums::VoteTipping::Strict,
            community_veto_vote_threshold: VoteThreshold::YesVotePercentage(55),
            reserved: [0; 3],
        };

        let token_owner_record_address = get_token_owner_record_address(
            &self.governance_program_id,
            &realm_address,
            &governing_token_mint_keypair.pubkey(),
            &token_owner.pubkey(),
        );

        let voter_weight_record = if self.voter_weight_addin_id.is_some() {
            let voter_weight_record = Keypair::new();
            let deposit_voter_weight_ix = setup_voter_weight_record(
                &self.voter_weight_addin_id.unwrap(),
                &realm_address,
                &governing_token_mint_keypair.pubkey(),
                &token_owner.pubkey(),
                &voter_weight_record.pubkey(),
                &self.bench.payer.pubkey(),
                amount,
                None,
                None,
                None,
            );

            self.bench
                .process_transaction(&[deposit_voter_weight_ix], Some(&[&voter_weight_record]))
                .await
                .unwrap();

            Some(voter_weight_record.pubkey())
        } else {
            None
        };

        let create_governance_ix = create_governance(
            &self.governance_program_id,
            &realm_address,
            Some(&governed_account_address),
            &token_owner_record_address,
            &self.bench.payer.pubkey(),
            &token_owner.pubkey(),
            voter_weight_record,
            governance_config,
        );

        self.bench
            .process_transaction(&[create_governance_ix], Some(&[&token_owner]))
            .await
            .unwrap();

        // Create Proposal

        let governance_address = get_governance_address(
            &self.governance_program_id,
            &realm_address,
            &governed_account_address,
        );

        let proposal_name = "Proposal #1".to_string();
        let description_link = "Proposal Description".to_string();
        let options = vec!["Yes".to_string()];
        let proposal_index: u32 = 0;
        let use_deny_option = true;

        let create_proposal_ix = create_proposal(
            &self.governance_program_id,
            &governance_address,
            &token_owner_record_address,
            &token_owner.pubkey(),
            &self.bench.payer.pubkey(),
            voter_weight_record,
            &realm_address,
            proposal_name,
            description_link.clone(),
            &governing_token_mint_keypair.pubkey(),
            VoteType::SingleChoice,
            options,
            use_deny_option,
            proposal_index,
        );

        self.bench
            .process_transaction(&[create_proposal_ix], Some(&[&token_owner]))
            .await
            .unwrap();

        let proposal_address = get_proposal_address(
            &self.governance_program_id,
            &governance_address,
            &governing_token_mint_keypair.pubkey(),
            &proposal_index.to_le_bytes(),
        );

        ProposalCookie {
            address: proposal_address,
            realm_address,
            governance_address,
            token_owner_record_address,
            token_owner,
            governing_token_mint: governing_token_mint_keypair.pubkey(),
            governing_token_mint_authority,
            voter_weight_record,
        }
    }

    #[allow(dead_code)]
    pub async fn with_token_owner_deposit(
        &mut self,
        proposal_cookie: &ProposalCookie,
        deposit_amount: u64,
    ) -> TokenOwnerRecordCookie {
        let token_owner = Keypair::new();
        let token_source = Keypair::new();

        let transfer_authority = Keypair::new();

        self.bench
            .create_token_account_with_transfer_authority(
                &token_source,
                &proposal_cookie.governing_token_mint,
                &proposal_cookie.governing_token_mint_authority,
                deposit_amount,
                &token_owner,
                &transfer_authority.pubkey(),
            )
            .await;

        let deposit_governing_tokens_ix = deposit_governing_tokens(
            &self.governance_program_id,
            &proposal_cookie.realm_address,
            &token_source.pubkey(),
            &token_owner.pubkey(),
            &token_owner.pubkey(),
            &self.bench.payer.pubkey(),
            deposit_amount,
            &proposal_cookie.governing_token_mint,
        );

        self.bench
            .process_transaction(&[deposit_governing_tokens_ix], Some(&[&token_owner]))
            .await
            .unwrap();

        let token_owner_record_address = get_token_owner_record_address(
            &self.governance_program_id,
            &proposal_cookie.realm_address,
            &proposal_cookie.governing_token_mint,
            &token_owner.pubkey(),
        );
        TokenOwnerRecordCookie {
            address: token_owner_record_address,
            token_owner,
        }
    }

    #[allow(dead_code)]
    pub async fn with_chat_message(
        &mut self,
        proposal_cookie: &ProposalCookie,
        reply_to: Option<Pubkey>,
    ) -> Result<ChatMessageCookie, ProgramError> {
        let message_account = Keypair::new();
        let message_body = MessageBody::Text("My comment".to_string());

        let post_message_ix = post_message(
            &self.program_id,
            &self.governance_program_id,
            &proposal_cookie.realm_address,
            &proposal_cookie.governance_address,
            &proposal_cookie.address,
            &proposal_cookie.token_owner_record_address,
            &proposal_cookie.token_owner.pubkey(),
            reply_to,
            &message_account.pubkey(),
            &self.bench.payer.pubkey(),
            proposal_cookie.voter_weight_record,
            message_body.clone(),
        );

        let clock = self.bench.get_clock().await;

        let message = ChatMessage {
            account_type: GovernanceChatAccountType::ChatMessage,
            proposal: proposal_cookie.address,
            author: proposal_cookie.token_owner.pubkey(),
            posted_at: clock.unix_timestamp,
            reply_to,
            body: message_body,
        };

        self.bench
            .process_transaction(
                &[post_message_ix],
                Some(&[&proposal_cookie.token_owner, &message_account]),
            )
            .await?;

        Ok(ChatMessageCookie {
            address: message_account.pubkey(),
            account: message,
        })
    }

    #[allow(dead_code)]
    pub async fn get_message_account(&mut self, message_address: &Pubkey) -> ChatMessage {
        self.bench
            .get_borsh_account::<ChatMessage>(message_address)
            .await
    }
}
